Java tutorial
/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.util.fxdesigner; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.ResourceBundle; import java.util.Stack; import java.util.stream.Collectors; import org.apache.commons.io.IOUtils; import org.reactfx.value.Val; import net.sourceforge.pmd.lang.LanguageVersion; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.symboltable.NameDeclaration; import net.sourceforge.pmd.lang.symboltable.NameOccurrence; import net.sourceforge.pmd.util.fxdesigner.model.XPathEvaluationException; import net.sourceforge.pmd.util.fxdesigner.util.DesignerUtil; import net.sourceforge.pmd.util.fxdesigner.util.LimitedSizeStack; import net.sourceforge.pmd.util.fxdesigner.util.TextAwareNodeWrapper; import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsOwner; import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsPersistenceUtil; import net.sourceforge.pmd.util.fxdesigner.util.beans.SettingsPersistenceUtil.PersistentProperty; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.application.Platform; import javafx.beans.property.DoubleProperty; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.ChoiceBox; import javafx.scene.control.CustomMenuItem; import javafx.scene.control.Label; import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.SplitPane; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.control.TextArea; import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; import javafx.stage.FileChooser; import javafx.util.Duration; /** * Main controller of the app. Mediator for subdivisions of the UI. * * @author Clment Fournier * @see NodeInfoPanelController * @see SourceEditorController * @see EventLogController * @see XPathPanelController * @since 6.0.0 */ @SuppressWarnings("PMD.UnusedPrivateField") public class MainDesignerController implements Initializable, SettingsOwner { /** * Callback to the owner. */ private final DesignerRoot designerRoot; /* Menu bar */ @FXML private MenuItem setupAuxclasspathMenuItem; @FXML private MenuItem openFileMenuItem; @FXML private MenuItem licenseMenuItem; @FXML private Menu openRecentMenu; @FXML private MenuItem exportToTestCodeMenuItem; @FXML private MenuItem exportXPathMenuItem; @FXML private Menu fileMenu; /* Center toolbar */ @FXML private ChoiceBox<LanguageVersion> languageChoiceBox; @FXML private ChoiceBox<String> xpathVersionChoiceBox; @FXML private ToggleButton bottomTabsToggle; /* Bottom panel */ @FXML private TabPane bottomTabPane; @FXML private Tab eventLogTab; @FXML private Tab xpathEditorTab; @FXML private SplitPane mainHorizontalSplitPane; /* Children */ @FXML private NodeInfoPanelController nodeInfoPanelController; @FXML private XPathPanelController xpathPanelController; @FXML private SourceEditorController sourceEditorController; @FXML private EventLogController eventLogPanelController; // Other fields private Stack<File> recentFiles = new LimitedSizeStack<>(5); // Properties private Val<LanguageVersion> languageVersion = Val.constant(DesignerUtil.defaultLanguageVersion()); private Val<String> xpathVersion = Val.constant(DesignerUtil.defaultXPathVersion()); public MainDesignerController(DesignerRoot owner) { this.designerRoot = owner; } @Override public void initialize(URL location, ResourceBundle resources) { try { SettingsPersistenceUtil.restoreProperties(this, DesignerUtil.getSettingsFile()); } catch (Exception e) { // shouldn't prevent the app from opening // in case the file is corrupted, it will be overwritten on shutdown e.printStackTrace(); } initializeLanguageVersionMenu(); initializeViewAnimation(); xpathPanelController.initialiseVersionChoiceBox(xpathVersionChoiceBox); languageVersion = Val.wrap(languageChoiceBox.getSelectionModel().selectedItemProperty()); DesignerUtil.rewire(sourceEditorController.languageVersionProperty(), languageVersion, this::setLanguageVersion); xpathVersion = Val.wrap(xpathVersionChoiceBox.getSelectionModel().selectedItemProperty()); DesignerUtil.rewire(xpathPanelController.xpathVersionProperty(), xpathVersion, this::setXpathVersion); licenseMenuItem.setOnAction(e -> showLicensePopup()); openFileMenuItem.setOnAction(e -> onOpenFileClicked()); openRecentMenu.setOnAction(e -> updateRecentFilesMenu()); openRecentMenu.setOnShowing(e -> updateRecentFilesMenu()); fileMenu.setOnShowing(e -> onFileMenuShowing()); exportXPathMenuItem.setOnAction(e -> { try { xpathPanelController.showExportXPathToRuleWizard(); } catch (IOException e1) { e1.printStackTrace(); } }); setupAuxclasspathMenuItem.setOnAction(e -> sourceEditorController.showAuxclasspathSetupPopup(designerRoot)); Platform.runLater(this::updateRecentFilesMenu); Platform.runLater(this::refreshAST); // initial refreshing Platform.runLater(() -> sourceEditorController.moveCaret(0, 0)); Platform.runLater(() -> { // fixes choicebox bad rendering on first opening languageChoiceBox.show(); languageChoiceBox.hide(); }); } private void initializeLanguageVersionMenu() { List<LanguageVersion> supported = DesignerUtil.getSupportedLanguageVersions(); supported.sort(LanguageVersion::compareTo); languageChoiceBox.getItems().addAll(supported); languageChoiceBox.setConverter(DesignerUtil.languageVersionStringConverter()); languageChoiceBox.getSelectionModel().select(DesignerUtil.defaultLanguageVersion()); languageChoiceBox.show(); } private void initializeViewAnimation() { // gets captured in the closure final double defaultMainHorizontalSplitPaneDividerPosition = mainHorizontalSplitPane .getDividerPositions()[0]; // show/ hide bottom pane bottomTabsToggle.selectedProperty().addListener((observable, wasExpanded, isNowExpanded) -> { KeyValue keyValue = null; DoubleProperty divPosition = mainHorizontalSplitPane.getDividers().get(0).positionProperty(); if (wasExpanded && !isNowExpanded) { keyValue = new KeyValue(divPosition, 1); } else if (!wasExpanded && isNowExpanded) { keyValue = new KeyValue(divPosition, defaultMainHorizontalSplitPaneDividerPosition); } if (keyValue != null) { Timeline timeline = new Timeline(new KeyFrame(Duration.millis(200), keyValue)); timeline.play(); } }); } public void shutdown() { try { SettingsPersistenceUtil.persistProperties(this, DesignerUtil.getSettingsFile()); } catch (IOException ioe) { // nevermind ioe.printStackTrace(); } } /** * Attempts to refresh the AST with the up-to-date source, * also updating XPath results. */ public void refreshAST() { Optional<Node> root = sourceEditorController.refreshAST(); if (root.isPresent()) { xpathPanelController.evaluateXPath(root.get(), getLanguageVersion()); } else { xpathPanelController.invalidateResults(true); } } /** * Refreshes the XPath results if the compilation unit is valid. * Otherwise does nothing. */ public void refreshXPathResults() { sourceEditorController.getCompilationUnit() .ifPresent(root -> xpathPanelController.evaluateXPath(root, getLanguageVersion())); } /** * Returns a wrapper around the given node that gives access * to its textual representation in the editor area. */ public TextAwareNodeWrapper wrapNode(Node node) { return sourceEditorController.wrapNode(node); } /** * Executed when the user selects a node in a treeView or listView. */ public void onNodeItemSelected(Node selectedValue) { nodeInfoPanelController.displayInfo(selectedValue); sourceEditorController.setFocusNode(selectedValue); sourceEditorController.focusNodeInTreeView(selectedValue); } public void onNameDeclarationSelected(NameDeclaration declaration) { sourceEditorController.clearNameOccurences(); List<NameOccurrence> occurrences = declaration.getNode().getScope().getDeclarations().get(declaration); if (occurrences != null) { sourceEditorController.highlightNameOccurrences(occurrences); } Platform.runLater(() -> onNodeItemSelected(declaration.getNode())); } /** * Runs an XPath (2.0) query on the current AST. * Performs no side effects. * * @param query the query * @return the matched nodes * @throws XPathEvaluationException if the query fails */ public List<Node> runXPathQuery(String query) throws XPathEvaluationException { return sourceEditorController.getCompilationUnit() .map(n -> xpathPanelController.runXPathQuery(n, getLanguageVersion(), query)) .orElseGet(Collections::emptyList); } /** * Handles nodes that potentially caused an error. * This can for example highlight nodes on the * editor. Effects can be reset with {@link #resetSelectedErrorNodes()}. * * @param n Node */ public void handleSelectedNodeInError(List<Node> n) { resetSelectedErrorNodes(); sourceEditorController.highlightErrorNodes(n); } public void resetSelectedErrorNodes() { sourceEditorController.clearErrorNodes(); } public void resetXPathResults() { sourceEditorController.clearXPathHighlight(); } /** Replaces previously highlighted XPath results with the given nodes. */ public void highlightXPathResults(List<Node> nodes) { sourceEditorController.highlightXPathResults(nodes); } private void showLicensePopup() { Alert licenseAlert = new Alert(AlertType.INFORMATION); licenseAlert.setWidth(500); licenseAlert.setHeaderText("License"); ScrollPane scroll = new ScrollPane(); try { scroll.setContent(new TextArea(IOUtils.toString(getClass().getResourceAsStream("LICENSE")))); } catch (IOException e) { e.printStackTrace(); } licenseAlert.getDialogPane().setContent(scroll); licenseAlert.showAndWait(); } private void onFileMenuShowing() { openRecentMenu.setDisable(recentFiles.isEmpty()); } private void onOpenFileClicked() { FileChooser chooser = new FileChooser(); chooser.setTitle("Load source from file"); File file = chooser.showOpenDialog(designerRoot.getMainStage()); loadSourceFromFile(file); } private void loadSourceFromFile(File file) { if (file != null) { try { String source = IOUtils.toString(new FileInputStream(file)); sourceEditorController.setText(source); LanguageVersion guess = DesignerUtil.getLanguageVersionFromExtension(file.getName()); if (guess != null) { // guess the language from the extension languageChoiceBox.getSelectionModel().select(guess); refreshAST(); } recentFiles.push(file); } catch (IOException e) { e.printStackTrace(); } } } private void updateRecentFilesMenu() { List<MenuItem> items = new ArrayList<>(); List<File> filesToClear = new ArrayList<>(); for (final File f : recentFiles) { if (f.exists() && f.isFile()) { CustomMenuItem item = new CustomMenuItem(new Label(f.getName())); item.setOnAction(e -> loadSourceFromFile(f)); item.setMnemonicParsing(false); Tooltip.install(item.getContent(), new Tooltip(f.getAbsolutePath())); items.add(item); } else { filesToClear.add(f); } } recentFiles.removeAll(filesToClear); if (items.isEmpty()) { openRecentMenu.setDisable(true); return; } Collections.reverse(items); items.add(new SeparatorMenuItem()); MenuItem clearItem = new MenuItem(); clearItem.setText("Clear menu"); clearItem.setOnAction(e -> { recentFiles.clear(); openRecentMenu.setDisable(true); }); items.add(clearItem); openRecentMenu.getItems().setAll(items); } public void invalidateAst() { nodeInfoPanelController.invalidateInfo(); xpathPanelController.invalidateResults(false); sourceEditorController.setFocusNode(null); } public LanguageVersion getLanguageVersion() { return languageVersion.getValue(); } public void setLanguageVersion(LanguageVersion version) { if (languageChoiceBox.getItems().contains(version)) { languageChoiceBox.getSelectionModel().select(version); } } public Val<LanguageVersion> languageVersionProperty() { return languageVersion; } public String getXpathVersion() { return xpathVersion.getValue(); } public void setXpathVersion(String version) { if (xpathVersionChoiceBox.getItems().contains(version)) { xpathVersionChoiceBox.getSelectionModel().select(version); } } public Val<String> xpathVersionProperty() { return xpathVersion; } @PersistentProperty public String getRecentFiles() { return recentFiles.stream().map(File::getAbsolutePath).collect(Collectors.joining(File.pathSeparator)); } public void setRecentFiles(String files) { Arrays.stream(files.split(File.pathSeparator)).map(File::new).forEach(recentFiles::push); } @PersistentProperty public boolean isMaximized() { return designerRoot.getMainStage().isMaximized(); } public void setMaximized(boolean b) { designerRoot.getMainStage().setMaximized(!b); // trigger change listener anyway designerRoot.getMainStage().setMaximized(b); } @PersistentProperty public boolean isBottomTabExpanded() { return bottomTabsToggle.isSelected(); } public void setBottomTabExpanded(boolean b) { bottomTabsToggle.setSelected(b); } @PersistentProperty public int getBottomTabIndex() { return bottomTabPane.getSelectionModel().getSelectedIndex(); } public void setBottomTabIndex(int i) { bottomTabPane.getSelectionModel().select(i); } @Override public List<SettingsOwner> getChildrenSettingsNodes() { return Arrays.asList(xpathPanelController, sourceEditorController); } }