diff --git a/gui/src/main/java/com/devonfw/ide/gui/App.java b/gui/src/main/java/com/devonfw/ide/gui/App.java index 3094e5185c..4809e232c9 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/App.java +++ b/gui/src/main/java/com/devonfw/ide/gui/App.java @@ -2,6 +2,7 @@ import java.io.IOException; import javafx.application.Application; +import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.geometry.Rectangle2D; import javafx.scene.Parent; @@ -10,6 +11,10 @@ import javafx.stage.Screen; import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.ide.gui.modal.IdeDialog; import com.devonfw.tools.ide.variable.IdeVariables; import com.devonfw.tools.ide.version.IdeVersion; @@ -20,9 +25,17 @@ public class App extends Application { Parent root; + private static final Logger LOG = LoggerFactory.getLogger(App.class); + @Override public void start(Stage primaryStage) throws IOException { + Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { + LOG.info("Uncaught exception in thread {}: {}", thread.getName(), throwable.getMessage(), throwable); + Platform.runLater(() -> new IdeDialog(IdeDialog.AlertType.ERROR, throwable.getMessage()).showAndWait()); + } + ); + FXMLLoader fxmlLoader = new FXMLLoader(App.class.getResource("main-view.fxml")); fxmlLoader.setController( new MainController(System.getenv(IdeVariables.IDE_ROOT.getName())) diff --git a/gui/src/main/java/com/devonfw/ide/gui/AppLauncher.java b/gui/src/main/java/com/devonfw/ide/gui/AppLauncher.java index aac16bfb9b..916f511b98 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/AppLauncher.java +++ b/gui/src/main/java/com/devonfw/ide/gui/AppLauncher.java @@ -2,10 +2,11 @@ /** * Launcher class for the App. Workaround for "Error: JavaFX runtime components are missing, and are required to run this application." Inspired by - * StackOverflow + * StackOverflow */ public class AppLauncher { + @SuppressWarnings("MissingJavadoc") public static void main(final String[] args) { App.main(args); diff --git a/gui/src/main/java/com/devonfw/ide/gui/MainController.java b/gui/src/main/java/com/devonfw/ide/gui/MainController.java index e153149107..333ae4a494 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/MainController.java +++ b/gui/src/main/java/com/devonfw/ide/gui/MainController.java @@ -1,9 +1,8 @@ package com.devonfw.ide.gui; -import java.io.IOException; -import java.nio.file.Files; +import java.io.FileNotFoundException; import java.nio.file.Path; -import java.util.stream.Stream; +import java.util.List; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; @@ -11,10 +10,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.devonfw.ide.gui.context.IdeGuiStateManager; +import com.devonfw.ide.gui.context.ProjectManager; +import com.devonfw.ide.gui.modal.IdeDialog; import com.devonfw.tools.ide.context.IdeContext; -import com.devonfw.tools.ide.context.IdeStartContextImpl; -import com.devonfw.tools.ide.log.IdeLogLevel; -import com.devonfw.tools.ide.log.IdeLogListenerBuffer; /** * Controller of the main screen of the dashboard GUI. @@ -23,6 +22,9 @@ public class MainController { private static Logger LOG = LoggerFactory.getLogger(MainController.class); + private ProjectManager projectManager; + + @FXML private ComboBox selectedProject; @@ -49,8 +51,11 @@ public class MainController { * Constructor */ public MainController(String directoryPath) { + LOG.debug("IDE_ROOT path={}", directoryPath); this.directoryPath = directoryPath; + + this.projectManager = IdeGuiStateManager.getInstance().getProjectManager(); } @FXML @@ -88,21 +93,10 @@ private void setProjectsComboBox() { assert (directoryPath != null) : "directoryPath is null! Please check the setup of your environment variables (IDE_ROOT)"; + List projects = projectManager.getProjectNames(); + selectedProject.getItems().clear(); - Path directory = Path.of(directoryPath); - - if (Files.exists(directory) && Files.isDirectory(directory)) { - try (Stream subPaths = Files.list(directory)) { - subPaths - .filter(Files::isDirectory) - .map(Path::getFileName) - .map(Path::toString) - .filter(name -> !name.startsWith("_")) - .forEach(name -> selectedProject.getItems().add(name)); - } catch (IOException e) { - throw new IllegalStateException("Failed to list projects!", e); - } - } + selectedProject.getItems().addAll(projects); selectedProject.setOnAction(actionEvent -> { @@ -114,35 +108,38 @@ private void setProjectsComboBox() { vsCodeOpen.setDisable(false); selectedWorkspace.setValue("main"); this.workspaceValue = Path.of("main"); + updateContext(selectedProject.getValue(), selectedWorkspace.getValue()); }); } @FXML private void setWorkspaceValue() { + List workspaces = projectManager.getWorkspaceNames(selectedProject.getValue()); + selectedWorkspace.getItems().clear(); - Path directory = Path.of(directoryPath).resolve(projectValue); - if (Files.exists(directory) && Files.isDirectory(directory)) { - try (Stream subPaths = Files.list(directory)) { - subPaths - .filter(Files::isDirectory) - .map(Path::getFileName) - .map(Path::toString) - .forEach(name -> selectedWorkspace.getItems().add(name)); - - } catch (IOException e) { - throw new RuntimeException("Error occurred while fetching workspace names.", e); - } - } + selectedWorkspace.getItems().addAll(workspaces); + this.workspaceValue = Path.of(selectedWorkspace.getValue()); + updateContext(selectedProject.getValue(), selectedWorkspace.getValue()); } private void openIDE(String inIde) { - final IdeLogListenerBuffer buffer = new IdeLogListenerBuffer(); - IdeLogLevel logLevel = IdeLogLevel.INFO; - IdeStartContextImpl startContext = new IdeStartContextImpl(logLevel, buffer); - IdeGuiContext context = new IdeGuiContext(startContext, Path.of(this.directoryPath).resolve(this.projectValue).resolve(this.workspaceValue)); - context.getCommandletManager().getCommandlet(inIde).run(); + IdeGuiStateManager + .getInstance() + .getCurrentContext() + .getCommandletManager() + .getCommandlet(inIde) + .run(); + } + + private void updateContext(String selectedProjectName, String selectedWorkspaceName) { + try { + IdeGuiStateManager.getInstance().switchContext(selectedProjectName, selectedWorkspaceName); + } catch (FileNotFoundException e) { + IdeDialog errorDialog = new IdeDialog(IdeDialog.AlertType.ERROR, e.getMessage()); + errorDialog.showAndWait(); + } } } diff --git a/gui/src/main/java/com/devonfw/ide/gui/IdeGuiContext.java b/gui/src/main/java/com/devonfw/ide/gui/context/IdeGuiContext.java similarity index 96% rename from gui/src/main/java/com/devonfw/ide/gui/IdeGuiContext.java rename to gui/src/main/java/com/devonfw/ide/gui/context/IdeGuiContext.java index c16732d70a..221e9325c3 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/IdeGuiContext.java +++ b/gui/src/main/java/com/devonfw/ide/gui/context/IdeGuiContext.java @@ -1,4 +1,4 @@ -package com.devonfw.ide.gui; +package com.devonfw.ide.gui.context; import java.nio.file.Path; @@ -12,7 +12,6 @@ */ public class IdeGuiContext extends AbstractIdeContext { - /** * The constructor. * @@ -20,17 +19,19 @@ public class IdeGuiContext extends AbstractIdeContext { * @param workingDirectory the optional {@link Path} to current working directory. */ public IdeGuiContext(IdeStartContextImpl startContext, Path workingDirectory) { + super(startContext, workingDirectory); } @Override protected String readLine() { + return ""; } @Override public IdeProgressBar newProgressBar(String title, long size, String unitName, long unitSize) { + return new IdeProgressBarNone(title, 0, unitName, unitSize); } - } diff --git a/gui/src/main/java/com/devonfw/ide/gui/context/IdeGuiStateManager.java b/gui/src/main/java/com/devonfw/ide/gui/context/IdeGuiStateManager.java new file mode 100644 index 0000000000..b0e3f05393 --- /dev/null +++ b/gui/src/main/java/com/devonfw/ide/gui/context/IdeGuiStateManager.java @@ -0,0 +1,109 @@ +package com.devonfw.ide.gui.context; + +import java.io.FileNotFoundException; +import java.nio.file.Path; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.tools.ide.context.IdeStartContextImpl; +import com.devonfw.tools.ide.log.IdeLogLevel; +import com.devonfw.tools.ide.log.IdeLogListenerBuffer; + +/** + * This class has the purpose of enabling the context state management for the IDEasy GUI. It is a thread-safe singleton implementation (Bill Pugh Singleton). + */ +public class IdeGuiStateManager { + + private static final Logger LOG = LoggerFactory.getLogger(IdeGuiStateManager.class); + + private String projectDirectory; + + private ProjectManager projectManager; + + /** + * Project context based on which project the user works in. + */ + private IdeGuiContext currentContext; + + private IdeGuiStateManager() { + + this.projectDirectory = System.getenv("IDE_ROOT"); + + if (this.projectDirectory == null) { + LOG.warn("IDE_ROOT environment variable is not set! Any dependent operation will fail before switchContext is called"); + } + + this.projectManager = new ProjectManager(Path.of(projectDirectory)); + } + + /** + * @return the singleton instance of the {@link IdeGuiStateManager}. + */ + public static IdeGuiStateManager getInstance() { + return Holder.INSTANCE; + } + + /** + * @param projectName name of the project folder + * @param workspaceName name of the workspace folder + * @return the new {@link IdeGuiContext} for the selected project and workspace. + * @throws FileNotFoundException if workspace or project does not exist + */ + public IdeGuiContext switchContext(String projectName, String workspaceName) throws FileNotFoundException { + + LOG.debug("Switching context to project {} and workspace {}", projectName, workspaceName); + + Path workspacePath = Path.of(projectDirectory, projectName, "workspaces", workspaceName); + + if (!workspacePath.toFile().exists()) { + throw new FileNotFoundException("Workspace " + workspacePath + " does not exist!"); + } + + final IdeLogListenerBuffer buffer = new IdeLogListenerBuffer(); + IdeLogLevel logLevel = IdeLogLevel.DEBUG; + IdeStartContextImpl startContext = new IdeStartContextImpl(logLevel, buffer); + this.currentContext = new IdeGuiContext(startContext, workspacePath); + return this.currentContext; + } + + /** + * This variant of the {@link #switchContext(String, String)} method is used when the IDE_ROOT environment variable has to be set manually. USE WITH CARE. + * (e.g. in tests) + * + * @param rootDirectory root directory for the ide projects. + * @param projectName 1st level folder of the project + * @param workspaceName used workspace + * @return the new {@link IdeGuiContext} for the selected project and workspace. + * @throws FileNotFoundException id either the specified project folder or workspace does not exist. + */ + public IdeGuiContext switchContext(Path rootDirectory, String projectName, String workspaceName) throws FileNotFoundException { + + this.projectDirectory = rootDirectory.toString(); + this.projectManager = new ProjectManager(rootDirectory); + + return switchContext(projectName, workspaceName); + } + + /** + * @return the current {@link IdeGuiContext} based on the selected project. + */ + public IdeGuiContext getCurrentContext() { + + return this.currentContext; + } + + public ProjectManager getProjectManager() { + return projectManager; + } + + /** + * Holder class for the singleton instance. The static keyword ensures the thread-safety of the singleton. + * + * @see More info + */ + private static class Holder { + + private static final IdeGuiStateManager INSTANCE = new IdeGuiStateManager(); + } +} diff --git a/gui/src/main/java/com/devonfw/ide/gui/context/ProjectManager.java b/gui/src/main/java/com/devonfw/ide/gui/context/ProjectManager.java new file mode 100644 index 0000000000..87354f5186 --- /dev/null +++ b/gui/src/main/java/com/devonfw/ide/gui/context/ProjectManager.java @@ -0,0 +1,118 @@ +package com.devonfw.ide.gui.context; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Stream; + +import org.jline.utils.Log; + +/** + * Service class that allows to access the list of projects + */ +public class ProjectManager { + + private final Path ideRootDirectory; + + private final ArrayList projectNames = new ArrayList<>(); + private final HashMap> workspaces = new HashMap<>(); + + /** + * Service class that reads the list of projects/workspaces + * + * @param ideRootDirectory IDE_ROOT ENV variable value + */ + /* + * Protected: Class should only be accessed by the code via the {@link IdeGuiStateManager} + * Why not in the IdeGuiContext? Reasoning is, that you might want to read the list of projects without being already in the project context + */ + protected ProjectManager(Path ideRootDirectory) { + + this.ideRootDirectory = ideRootDirectory; + + if (ideRootDirectory == null) { + throw new IllegalArgumentException("Root directory is null"); + } else if (!Files.exists(ideRootDirectory)) { + throw new IllegalArgumentException("Root directory does not exist"); + } else if (!Files.isDirectory(ideRootDirectory)) { + throw new IllegalArgumentException("Root directory is not a directory"); + } + + refreshProjects(); + } + + /** + * re-reads the list of projects/workspaces + */ + public void refreshProjects() { + projectNames.clear(); + workspaces.clear(); + + readProjects(); + readWorkspaces(); + } + + /** + * read all projects in the users IDE_ROOT directory + */ + private void readProjects() { + + try (Stream subPaths = Files.list(ideRootDirectory)) { + subPaths + .filter(Files::isDirectory) + .map(Path::getFileName) + .map(Path::toString) + .filter(name -> !name.startsWith("_") && Files.exists(ideRootDirectory.resolve(name).resolve("workspaces"))) + .forEach(projectNames::add); + } catch (IOException e) { + Log.error("Failed to read project list!", e); + throw new IllegalStateException("Failed to read project list!", e); + } + } + + /** + * reads all workspaces of all loaded projects. + */ + protected void readWorkspaces() { + + assert !projectNames.isEmpty() : "No projects initialized, cannot read workspaces!"; + + for (String projectName : projectNames) { + Path projectDirectory = ideRootDirectory.resolve(projectName); + Path workspacesDirectory = projectDirectory.resolve("workspaces"); + + ArrayList workspaceNames = new ArrayList<>(); + + try (Stream subPaths = Files.list(workspacesDirectory)) { + subPaths + .filter(Files::isDirectory) + .map(Path::getFileName) + .map(Path::toString) + .forEach(workspaceNames::add); + + workspaces.put(projectName, workspaceNames); + } catch (IOException e) { + Log.error("Error occurred while fetching workspace names.", e); + throw new RuntimeException("Error occurred while fetching workspace names.", e); + } + } + } + + /** + * @return the list of project (names) in the project directory + */ + public List getProjectNames() { + return projectNames; + } + + /** + * @param projectName name of the project for which the workspace names should be returned + * @return the list of workspace (names) for the given project name + */ + public List getWorkspaceNames(String projectName) { + return workspaces.get(projectName); + } +} diff --git a/gui/src/main/java/com/devonfw/ide/gui/modal/IdeDialog.java b/gui/src/main/java/com/devonfw/ide/gui/modal/IdeDialog.java new file mode 100644 index 0000000000..2ee467ae23 --- /dev/null +++ b/gui/src/main/java/com/devonfw/ide/gui/modal/IdeDialog.java @@ -0,0 +1,42 @@ +package com.devonfw.ide.gui.modal; + +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.image.Image; +import javafx.stage.Stage; + +/** + * Custom Alert class for IDEasy to allow interaction via the CLIs questions/modals/selections. + */ +public class IdeDialog extends Alert { + + /** + * @param alertType the {@link AlertType} of the alert (e.g. INFORMATION, CONFIRMATION, etc). + */ + public IdeDialog(AlertType alertType) { + + super(alertType); + setupDefaultProperties(); + } + + /** + * @param alertType the {@link AlertType} of the alert (e.g. INFORMATION, CONFIRMATION, etc). + * @param message main message displayed in the dialoge + * @param buttonTypes defines the different buttons that the alert displays. + */ + public IdeDialog(AlertType alertType, String message, ButtonType... buttonTypes) { + + super(alertType, message, buttonTypes); + setupDefaultProperties(); + } + + private void setupDefaultProperties() { + + setTitle("IDEasy"); + + setOnShowing(event -> { + Stage stage = (Stage) getDialogPane().getScene().getWindow(); + stage.getIcons().add(new Image("com/devonfw/ide/gui/assets/devonfw.png")); + }); + } +} diff --git a/gui/src/main/resources/com/devonfw/ide/gui/main-view.fxml b/gui/src/main/resources/com/devonfw/ide/gui/main-view.fxml index eafdea657a..0ce3546a95 100644 --- a/gui/src/main/resources/com/devonfw/ide/gui/main-view.fxml +++ b/gui/src/main/resources/com/devonfw/ide/gui/main-view.fxml @@ -1,126 +1,129 @@ - - - - - - - - - - + + + + - - + - + - + - - + - - + - +
- - + + - + - + - + - - + - + - + - - + - + - + - diff --git a/gui/src/test/java/com/devonfw/ide/gui/AppBaseTest.java b/gui/src/test/java/com/devonfw/ide/gui/AppBaseTest.java index 423de657e0..f32f3f047a 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/AppBaseTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/AppBaseTest.java @@ -2,6 +2,7 @@ import static org.testfx.assertions.api.Assertions.assertThat; +import java.io.FileNotFoundException; import java.io.IOException; import java.net.URL; import java.nio.file.Path; @@ -15,10 +16,13 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.CleanupMode; import org.junit.jupiter.api.io.TempDir; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.devonfw.ide.gui.context.IdeGuiStateManager; + /** * Basic UI Test */ @@ -29,7 +33,7 @@ public class AppBaseTest extends HeadlessApplicationTest { private Button androidStudioOpen, eclipseOpen, intellijOpen, vsCodeOpen; private ComboBox selectedProject, selectedWorkspace; - @TempDir + @TempDir(cleanup = CleanupMode.ON_SUCCESS) private static Path temporayProjectDirectoryPath; @Override @@ -58,7 +62,7 @@ public void start(Stage stage) throws IOException { * to work in the test context. Generates a structure like this: /project-[0..6]/workspaces/main */ @BeforeAll - public static void generateProjectFolderStructure() { + protected static void generateProjectFolderStructure() throws FileNotFoundException { LOGGER.debug("tempDir: {}", temporayProjectDirectoryPath); for (int i = 0; i <= 5; i++) { @@ -74,7 +78,10 @@ public static void generateProjectFolderStructure() { "Unable to create mock main workspace directory for mock project " + i) .isTrue(); } - LOGGER.debug("project folders: {}", Arrays.toString(temporayProjectDirectoryPath.toFile().list())); + LOGGER.info("project folders: {}", Arrays.toString(temporayProjectDirectoryPath.toFile().list())); + + //We set the project root directory to the temporary directory before all tests, so that the IDE can find the projects in the test. + IdeGuiStateManager.getInstance().switchContext(temporayProjectDirectoryPath, "project-1", "main"); } /** diff --git a/gui/src/test/java/com/devonfw/ide/gui/HeadlessApplicationTest.java b/gui/src/test/java/com/devonfw/ide/gui/HeadlessApplicationTest.java index 2e7298caac..3b51b66787 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/HeadlessApplicationTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/HeadlessApplicationTest.java @@ -15,10 +15,11 @@ public abstract class HeadlessApplicationTest extends ApplicationTest { System.setProperty("testfx.robot", "glass"); System.setProperty("testfx.headless", "true"); - System.setProperty("glass.platform", "Monocle"); System.setProperty("prism.order", "sw"); System.setProperty("prism.text", "t2k"); - System.setProperty("testfx.setup.timeout", "10000"); // increased timeout for testing on server-side CIs System.setProperty("java.awt.headless", "true"); + System.setProperty("glass.platform", "Monocle"); + System.setProperty("monocle.platform", "Headless"); + System.setProperty("testfx.setup.timeout", "10000"); // increased timeout for testing on server-side CIs } } diff --git a/pom.xml b/pom.xml index d4651d71a6..962dbfe68e 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ 2.18.3 2.11.0 3.11.0 - 21 + 25 4.0.18 0.10.6 3.7.1