package de.eisfeldj.augendiagnosefx.controller; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.ResourceBundle; import de.eisfeldj.augendiagnosefx.util.DialogUtil; import de.eisfeldj.augendiagnosefx.util.DialogUtil.ConfirmDialogListener; import de.eisfeldj.augendiagnosefx.util.FxmlUtil; import de.eisfeldj.augendiagnosefx.util.Logger; import de.eisfeldj.augendiagnosefx.util.PreferenceUtil; import de.eisfeldj.augendiagnosefx.util.ResourceConstants; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.geometry.Orientation; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.control.Button; import javafx.scene.control.MenuBar; import javafx.scene.control.SplitPane; import javafx.scene.control.ToggleButton; import javafx.scene.image.ImageView; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import static de.eisfeldj.augendiagnosefx.util.PreferenceUtil.KEY_SHOW_COMMENT_PANE; import static de.eisfeldj.augendiagnosefx.util.PreferenceUtil.KEY_SHOW_SPLIT_WINDOW; /** * The controller of the main window. */ public class MainController extends BaseController implements Initializable { /** * The root of the main page. */ @FXML private VBox mMainPane; /** * The pane containing the body. */ @FXML private StackPane mBody; /** * The pane containing the menu bar. */ @FXML private MenuBar mMenuBar; /** * The pane containing menu buttons. */ @FXML private HBox mMenuButtons; /** * The onepane/twopane button in the menu bar. */ @FXML private ToggleButton mPaneButton; /** * The comment button in the menu bar. */ @FXML private ToggleButton mCommentButton; /** * The close button in the menu bar. */ @FXML private Button mCloseButton; /** * The save icon. */ @FXML private ImageView mImageSave; /** * The panes (one or two) containing the body. */ private StackPane[] mBodies = new StackPane[0]; /** * The list of subpages. */ private List<BaseController> mSubPageRegistry = new ArrayList<>(); /** * A list storing the handlers for closing windows. */ private List<EventHandler<ActionEvent>> mCloseHandlerList = new ArrayList<>(); /** * Indicator if two panes are shown. */ private boolean mIsSplitPane = false; /** * Get information if two panes are shown. * * @return True if two panes are shown. */ public final boolean isSplitPane() { return getInstance().mIsSplitPane; } /** * Set split pane. * * @param initialFill * The FXML file defining the initial content of the new pane. */ public final void setSplitPane(final String initialFill) { if (mIsSplitPane) { return; } mIsSplitPane = true; StackPane body1 = new StackPane(); StackPane body2 = new StackPane(); SplitPane splitPane = new SplitPane(); splitPane.setOrientation(Orientation.HORIZONTAL); splitPane.getItems().add(body1); splitPane.getItems().add(body2); mBodies = new StackPane[] {body1, body2, mBody}; if (initialFill != null) { FxmlUtil.displaySubpage(initialFill, 1, false); } // put lowest two elements to left pane. ObservableList<Node> children = mBody.getChildren(); if (children.size() > 0) { body1.getChildren().addAll(children.subList(0, Math.min(2, children.size()))); } // put other elements to right pane. if (children.size() > 0) { body2.getChildren().addAll(children); } children.clear(); children.add(splitPane); refreshSubPagesOnResize(); } /** * Set single pane. */ public final void setSinglePane() { if (!mIsSplitPane) { return; } mIsSplitPane = false; mBody.getChildren().clear(); List<BaseController> controllersUncloseable = new ArrayList<>(); List<BaseController> controllersPane0 = new ArrayList<>(); List<BaseController> controllersPane1 = new ArrayList<>(); for (BaseController controller : mSubPageRegistry) { if (controller.getPaneIndex() == 0) { if (controller.isCloseable()) { controllersPane0.add(controller); } else { controllersUncloseable.add(controller); } } else { if (controller.isCloseable()) { controllersPane1.add(controller); } } } for (int i = mSubPageRegistry.size() - 1; i >= 0; i--) { removeSubpage(mSubPageRegistry.get(i)); } mBodies = new StackPane[] {mBody}; for (BaseController controller : controllersUncloseable) { addSubPage(controller, 0, false); controller.addToRegistry(); } for (BaseController controller : controllersPane0) { addSubPage(controller, 0, true); controller.addToRegistry(); } for (BaseController controller : controllersPane1) { addSubPage(controller, 0, true); controller.addToRegistry(); } refreshSubPagesOnResize(); } /** * Handler for pane button. * * @param event * The action event. */ @FXML public final void toggleSplitWindow(final ActionEvent event) { MenuController.getInstance().toggleSplitWindow(event); } /** * Handler for comment button. * * @param event * The action event. */ @FXML public final void toggleCommentPane(final ActionEvent event) { MenuController.getInstance().toggleCommentPane(event); } /** * Refresh all subpages on resize. */ private void refreshSubPagesOnResize() { for (BaseController controller : mSubPageRegistry) { controller.refreshOnResize(); } } @Override public final Parent getRoot() { return mMainPane; } @Override public final void initialize(final URL location, final ResourceBundle resources) { mBodies = new StackPane[] {mBody}; mPaneButton.setSelected(PreferenceUtil.getPreferenceBoolean(KEY_SHOW_SPLIT_WINDOW)); mCommentButton.setSelected(PreferenceUtil.getPreferenceBoolean(KEY_SHOW_COMMENT_PANE)); } /** * Get the main controller instance. * * @return The main controller instance. */ public static MainController getInstance() { try { return getController(MainController.class); } catch (TooManyControllersException | MissingControllerException e) { Logger.error("Could not find main controller", e); throw new RuntimeException(e); } } /** * Set the contents of the menu bar. * * @param menuBarContents * The contents of the menu bar. */ public final void setMenuBarContents(final MenuBar menuBarContents) { mMenuBar.getMenus().clear(); mMenuBar.getMenus().addAll(menuBarContents.getMenus()); } /** * Add a subpage. * * @param controller * The controller of the subpage. * @param paneIndex * The pane where to add the subpage. * @param isCloseable * Indicator if this is a closable page. */ public final void addSubPage(final BaseController controller, final int paneIndex, final boolean isCloseable) { int effectivePaneIndex = paneIndex; if (paneIndex == -1) { effectivePaneIndex = isSplitPane() ? 2 : 0; } mBodies[effectivePaneIndex].getChildren().add(controller.getRoot()); controller.setPaneIndex(effectivePaneIndex); controller.setCloseable(isCloseable); int position; if (isCloseable) { position = mSubPageRegistry.size(); mSubPageRegistry.add(controller); } else { position = unclosablePagesCount(); mSubPageRegistry.add(position, controller); } // Enable the close menu. enableClose(new EventHandler<ActionEvent>() { @Override public void handle(final ActionEvent event) { if (controller.isDirty()) { ConfirmDialogListener listener = new ConfirmDialogListener() { @Override public void onDialogPositiveClick() { controller.setDirty(false); MainController.this.removeSubpage(controller); } @Override public void onDialogNegativeClick() { // do nothing. } }; DialogUtil.displayConfirmationMessage(listener, ResourceConstants.BUTTON_OK, ResourceConstants.MESSAGE_CONFIRM_EXIT_UNSAVED); } else { MainController.this.removeSubpage(controller); } } }, position); updatePaneButtonVisibility(); } /** * Remove a subpage. * * @param controller * The controller of the subpage. */ private void removeSubpage(final BaseController controller) { controller.close(); int index = mSubPageRegistry.indexOf(controller); mSubPageRegistry.remove(controller); disableClose(index); updatePaneButtonVisibility(); if (MainController.getInstance().isSplitPane() && !hasClosablePage()) { MainController.getInstance().setSinglePane(); } } /** * Remove all subpages. */ public final void removeAllSubPages() { for (BaseController controller : mSubPageRegistry) { controller.close(); } mSubPageRegistry.clear(); disableAllClose(); updatePaneButtonVisibility(); } /** * Enable the close menu item. * * @param eventHandler * The event handler to be called when closing. * @param position * The position in the stack. (-1 means end) */ private void enableClose(final EventHandler<ActionEvent> eventHandler, final int position) { if (position >= 0) { mCloseHandlerList.add(position, eventHandler); } else { mCloseHandlerList.add(eventHandler); } if (hasClosablePage()) { MenuController.getInstance().setMenuClose(true, eventHandler); mCloseButton.setVisible(true); mCloseButton.setOnAction(mCloseHandlerList.get(mCloseHandlerList.size() - 1)); } } /** * Disable one level of the close menu item. * * @param position * The position in the stack. (-1 means end) */ private void disableClose(final int position) { if (position >= 0) { mCloseHandlerList.remove(position); } else { mCloseHandlerList.remove(mCloseHandlerList.size() - 1); } if (hasClosablePage()) { EventHandler<ActionEvent> newEventHandler = mCloseHandlerList.get(mCloseHandlerList.size() - 1); MenuController.getInstance().setMenuClose(true, newEventHandler); mCloseButton.setOnAction(newEventHandler); } else { MenuController.getInstance().setMenuClose(false, null); mCloseButton.setVisible(false); } } /** * Check if there is a page that can be closed. * * @return true if there is a page that can be closed. */ public final boolean hasClosablePage() { return mSubPageRegistry.size() > unclosablePagesCount(); } /** * Get the number of unclosable pages. * * @return the number of unclosable pages. */ private int unclosablePagesCount() { int counter = 0; for (BaseController controller : mSubPageRegistry) { if (!controller.isCloseable()) { counter++; } } return counter; } /** * Disable all levels of the close menu icon. */ private void disableAllClose() { mCloseHandlerList.clear(); MenuController.getInstance().setMenuClose(false, null); mCloseButton.setVisible(false); } /** * Set the save icon visibility. * * @param visible the target visibility */ public static void setSaveIconVisibility(final boolean visible) { getInstance().mImageSave.setVisible(visible); } /** * Find out if there is any dirty page that requires saving. * * @return true if there is a dirty page. */ public static boolean hasDirtyBaseController() { for (BaseController controller : getInstance().mSubPageRegistry) { if (controller.isDirty()) { return true; } } return false; } /** * Set the status of the pane button. * * @param twopane true if it should display twopane icon. */ protected void setPaneButtonStatus(final boolean twopane) { mPaneButton.setSelected(twopane); } /** * Set the status of the comment button. * * @param commentPane true if it should display active comment pane icon. */ protected void setCommentButtonStatus(final boolean commentPane) { mCommentButton.setSelected(commentPane); } /** * Set the visibility of the comment button. * * @param visible true if it should be visible. */ protected void setCommentButtonVisibility(final boolean visible) { mCommentButton.setVisible(visible); } /** * Update the visibility of the pane button. */ private void updatePaneButtonVisibility() { boolean enabled = BaseController.getControllers(DisplayImagePairController.class).size() == 0 && BaseController.getControllers(DisplayImageFullController.class).size() == 0; mPaneButton.setVisible(enabled); MenuController.getInstance().setSplitWindowEnabled(enabled); } }