diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 918ac0f87e..d46485f28b 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -23,9 +23,12 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/822[#822] : Added Cwd path limit in shell * https://github.com/devonfw/IDEasy/issues/1689[#1689]: Fix misleading message in `set-version` when the version is already installed * https://github.com/devonfw/IDEasy/issues/1517[#1517]: Fix IDEasy MSI does not configure Windows Terminal for git-bash -* https://github.com/devonfw/IDEasy/issues/1227[#1227]: Added Setup instrcutions to the Windows installer +* https://github.com/devonfw/IDEasy/issues/1227[#1227]: Added Setup instructions to the Windows installer * https://github.com/devonfw/IDEasy/issues/2114[#2114]: Fix false "Cygwin is not supported" warning in Git Bash * https://github.com/devonfw/IDEasy/issues/865[#865]: az not working on Mac +* https://github.com/devonfw/IDEasy/issues/1936[#1936]: Improve localization of GUI +* https://github.com/devonfw/IDEasy/issues/2045[#2045]: Add tool selection and version or edition configuration to GUI + * https://github.com/devonfw/IDEasy/issues/2026[#2026]: Create UvRepository and UvBasedCommandlet The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/46?closed=1[milestone 2026.07.001]. @@ -100,8 +103,6 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/1904[#1904]: Add Inso CLI to IDEasy commandlets * https://github.com/devonfw/IDEasy/issues/1952[#1952]: Ability for platform specific dependencies * https://github.com/devonfw/IDEasy/issues/1950[#1950]: Fix exit autocompletion -* https://github.com/devonfw/IDEasy/issues/1936[#1936]: Improve localization of GUI - * https://github.com/devonfw/IDEasy/issues/1958[#1958]: Fix git pull on settings with local branch without remote The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/44?closed=1[milestone 2026.05.001]. 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 11e0f43993..61ca77cb0d 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/MainController.java +++ b/gui/src/main/java/com/devonfw/ide/gui/MainController.java @@ -7,8 +7,13 @@ import java.util.Locale; import java.util.Map; import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.Tooltip; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,6 +22,7 @@ import com.devonfw.ide.gui.context.ProjectManager; import com.devonfw.ide.gui.modal.IdeDialog; import com.devonfw.ide.gui.nls.NlsService; +import com.devonfw.ide.gui.settings.ToolSettingsController; /** * Controller of the main screen of the dashboard GUI. @@ -49,6 +55,15 @@ public class MainController { @FXML private Button vsCodeOpen; + @FXML + private TabPane tabPane; + + @FXML + private Tab mainTab; + + @FXML + private Tab toolConfigTab; + private final String directoryPath; private final Map languageMap; @@ -73,6 +88,13 @@ public MainController(String directoryPath, NlsService nlsService) { private void initialize() { setProjectsComboBox(); initLanguageComboBox(); + toolConfigTab.setDisable(true); + toolConfigTab.setTooltip(new Tooltip(nlsService.get("toolConfigDisabled"))); + tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) -> { + if (newTab == toolConfigTab) { + loadToolConfigContent(); + } + }); } private void initLanguageComboBox() { @@ -148,7 +170,6 @@ private void openVsCode() { openIDE("vscode"); } - private void setProjectsComboBox() { assert (directoryPath != null) : "directoryPath is null! Please check the setup of your environment variables (IDE_ROOT)"; @@ -163,6 +184,7 @@ private void setProjectsComboBox() { setWorkspaceComboBox(); selectedWorkspace.setDisable(false); + }); } @@ -185,9 +207,24 @@ private void setWorkspaceComboBox() { eclipseOpen.setDisable(false); intellijOpen.setDisable(false); vsCodeOpen.setDisable(false); + toolConfigTab.setDisable(false); + // If tool config is already open, reload it to reflect the new context + if (toolConfigTab.isSelected()) { + loadToolConfigContent(); + } + // Pre-warm ide-urls git repo in background so the first dropdown open is fast + Thread preWarm = new Thread(() -> { + try { + IdeGuiStateManager.getInstance().getCurrentContext().getUrls(); + } catch (Exception ignored) { + } + }); + preWarm.setDaemon(true); + preWarm.start(); }); } + private void openIDE(String inIde) { IdeGuiStateManager @@ -198,6 +235,22 @@ private void openIDE(String inIde) { .run(); } + private void loadToolConfigContent() { + + try { + ToolSettingsController controller = new ToolSettingsController(nlsService); + FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/devonfw/ide/gui/tools-config.fxml")); + loader.setController(controller); + loader.setResources(nlsService.getResourceBundle()); + Parent content = loader.load(); + controller.setOnClose(() -> tabPane.getSelectionModel().select(mainTab)); + toolConfigTab.setContent(content); + } catch (Exception e) { + LOG.error("Failed to load tool config view", e); + new IdeDialog(IdeDialog.AlertType.ERROR, e.getMessage()).showAndWait(); + } + } + private void updateContext(String selectedProjectName, String selectedWorkspaceName) { try { IdeGuiStateManager.getInstance().switchContext(selectedProjectName, selectedWorkspaceName); diff --git a/gui/src/main/java/com/devonfw/ide/gui/settings/ToolConfiguration.java b/gui/src/main/java/com/devonfw/ide/gui/settings/ToolConfiguration.java new file mode 100644 index 0000000000..e4dd93eafd --- /dev/null +++ b/gui/src/main/java/com/devonfw/ide/gui/settings/ToolConfiguration.java @@ -0,0 +1,117 @@ +package com.devonfw.ide.gui.settings; + +import java.util.List; +import java.util.Objects; + +/** + * Model representing the configurable settings for a single tool. + */ +public final class ToolConfiguration { + + private final String toolName; + + /** Group/type of the tool used for grouping in the UI. */ + private ToolGroup group; + + private boolean enabled; + + private String configuredVersion; + + private String configuredEdition; + + private boolean supportsEdition; + + private List availableEditions; + + private List availableVersions; + + //<------------ Constructor & Getters/Setters ------------> + + public ToolConfiguration(String toolName) { + this.toolName = Objects.requireNonNull(toolName); + } + + /** + * Tool groups used to group/sort tools in the UI. Order of declaration determines the default ordering. + */ + public enum ToolGroup { + IDE("toolGroupIde"), + LOCAL("toolGroupLocal"), + GLOBAL("toolGroupGlobal"), + PIP("toolGroupPip"), + NPM("toolGroupNpm"), + OTHER("toolGroupOther"); + + private final String label; + + ToolGroup(String label) { + this.label = label; + } + + public String getLabel() { + return this.label; + } + } + + public String getToolName() { + return this.toolName; + } + + public ToolGroup getGroup() { + return this.group; + } + + public void setGroup(ToolGroup group) { + this.group = group; + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getConfiguredVersion() { + return this.configuredVersion; + } + + public void setConfiguredVersion(String configuredVersion) { + this.configuredVersion = configuredVersion; + } + + public String getConfiguredEdition() { + return this.configuredEdition; + } + + public void setConfiguredEdition(String configuredEdition) { + this.configuredEdition = configuredEdition; + } + + public boolean isSupportsEdition() { + return this.supportsEdition; + } + + public void setSupportsEdition(boolean supportsEdition) { + this.supportsEdition = supportsEdition; + } + + public List getAvailableEditions() { + return this.availableEditions; + } + + public void setAvailableEditions(List availableEditions) { + this.availableEditions = availableEditions; + } + + public List getAvailableVersions() { + return this.availableVersions; + } + + public void setAvailableVersions(List availableVersions) { + this.availableVersions = availableVersions; + } + +} + diff --git a/gui/src/main/java/com/devonfw/ide/gui/settings/ToolSettingsController.java b/gui/src/main/java/com/devonfw/ide/gui/settings/ToolSettingsController.java new file mode 100644 index 0000000000..4b2b678cc1 --- /dev/null +++ b/gui/src/main/java/com/devonfw/ide/gui/settings/ToolSettingsController.java @@ -0,0 +1,479 @@ +package com.devonfw.ide.gui.settings; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.control.Tooltip; +import javafx.scene.control.TreeCell; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.Window; + +import com.devonfw.ide.gui.context.IdeGuiContext; +import com.devonfw.ide.gui.context.IdeGuiStateManager; +import com.devonfw.ide.gui.nls.NlsService; + +/** + * Controller for the Tools Configuration dialog. + */ +public class ToolSettingsController { + + @FXML + private TreeView toolsTree; + + @FXML + private Button saveButton; + + @FXML + private Button previewButton; + + private final ToolSettingsService service = new ToolSettingsService(); + + private IdeGuiContext currentContext; + + private final Set validationErrors = new HashSet<>(); + + private final NlsService nlsService; + + private Runnable onClose; + + /** + * Constructor + * + * @param nlsService the injected {@link NlsService} used for translations. + */ + public ToolSettingsController(NlsService nlsService) { + this.nlsService = nlsService; + } + + NlsService getNlsService() { + return this.nlsService; + } + + + @FXML + private void initialize() { + currentContext = IdeGuiStateManager.getInstance().getCurrentContext(); + + updateButtonStates(); + + List configurations = service.listToolConfigurations(currentContext); + + toolsTree.setRoot(buildToolTree(configurations)); + toolsTree.setShowRoot(false); + toolsTree.setCellFactory(tv -> new ToolTreeCell(this)); + + // Load editions in background to avoid blocking the UI thread on startup. + // Each tool's edition list is a directory scan, so we batch all tools in a single thread + // and do a single refresh() at the end instead of one per tool. + if (currentContext != null) { + Thread t = new Thread(() -> { + for (ToolConfiguration tc : configurations) { + List editions = service.loadEditionsForTool(tc.getToolName(), currentContext); + tc.setAvailableEditions(editions); + // Only show edition selector when there is a real choice (>1 option). + tc.setSupportsEdition(editions.size() > 1); + // Auto-select the sole edition so the user doesn't have to. + if (editions.size() == 1 && (tc.getConfiguredEdition() == null || tc.getConfiguredEdition().isBlank())) { + tc.setConfiguredEdition(editions.get(0)); + } + } + Platform.runLater(() -> toolsTree.refresh()); + }); + t.setDaemon(true); + t.start(); + } + } + + private TreeItem buildToolTree(List toolConfigurations) { + TreeItem root = new TreeItem<>(); + root.setExpanded(false); + for (ToolConfiguration.ToolGroup group : ToolConfiguration.ToolGroup.values()) { + List groupTools = toolConfigurations.stream().filter(tc -> tc.getGroup() == group).toList(); + if (!groupTools.isEmpty()) { + root.getChildren().add(createGroupItem(group, groupTools)); + } + } + return root; + } + + private TreeItem createGroupItem(ToolConfiguration.ToolGroup group, List groupTools) { + Label toolGroupLabel = new Label(this.nlsService.get(group.getLabel())); + toolGroupLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-padding: 6 0 6 0;"); + + TreeItem groupItem = new TreeItem<>(null); + groupItem.setGraphic(toolGroupLabel); + groupItem.setExpanded(true); + for (ToolConfiguration toolConfiguration : groupTools) { + groupItem.getChildren().add(new TreeItem<>(toolConfiguration)); + } + return groupItem; + } + + @FXML + private void onSave() { + List toolConfigurations = collectLeafConfigurations(); + service.applyAndSave(toolConfigurations, currentContext); + closeWindow(); + } + + @FXML + private void onPreview() { + List toolConfigurations = collectLeafConfigurations(); + String content = service.buildPreviewSettingsContent(toolConfigurations); + + TextArea area = new TextArea(content); + area.setEditable(false); + area.setPrefWidth(800); + area.setPrefHeight(400); + + VBox box = new VBox(8, area); + box.setPadding(new Insets(10)); + + Scene scene = new Scene(box); + Stage dialog = new Stage(); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.initOwner(toolsTree.getScene().getWindow()); + dialog.setTitle(nlsService.get("toolsConfigTitle") + " - " + nlsService.get("toolsConfigPreviewTitle")); + + Button save = new Button(nlsService.get("save")); + Button close = new Button(nlsService.get("cancel")); + save.setOnAction(event -> { + event.consume(); + service.applyAndSave(toolConfigurations, currentContext); + dialog.close(); + closeWindow(); + }); + close.setOnAction(event -> { + event.consume(); + dialog.close(); + }); + + HBox actions = new HBox(8, close, save); + actions.setAlignment(Pos.CENTER_RIGHT); + box.getChildren().add(actions); + + dialog.setScene(scene); + dialog.showAndWait(); + } + + private List collectLeafConfigurations() { + return toolsTree.getRoot().getChildren().stream().flatMap(groupNode -> groupNode.getChildren().stream()).map(TreeItem::getValue) + .filter(Objects::nonNull).collect(Collectors.toList()); + } + + @FXML + private void onCancel() { + closeWindow(); + } + + private void updateButtonStates() { + boolean hasErrors = !validationErrors.isEmpty(); + saveButton.setDisable(hasErrors); + previewButton.setDisable(hasErrors); + } + + public void setOnClose(Runnable onClose) { + this.onClose = onClose; + } + + private void closeWindow() { + if (onClose != null) { + onClose.run(); + return; + } + if (toolsTree == null || toolsTree.getScene() == null) { + return; + } + Window window = toolsTree.getScene().getWindow(); + if (window != null) { + window.hide(); + } + } + + /** + * Custom cell rendering each tool row as: [checkbox | tool name | edition combo | version combo | error icon]. Group header rows carry a null value and are + * rendered via the graphic set on their TreeItem instead. + */ + private static final class ToolTreeCell extends TreeCell { + + private final HBox root; + private final ToolSettingsController controller; + private CheckBox enabled; + private Label name; + private ComboBox edition; + private ComboBox version; + private Label errorIcon; + + ToolTreeCell(ToolSettingsController controller) { + this.controller = controller; + this.root = new HBox(10); + this.root.setAlignment(Pos.CENTER_LEFT); + this.root.setStyle("-fx-padding: 5 0 5 0;"); + } + + @Override + protected void updateItem(ToolConfiguration toolItem, boolean empty) { + super.updateItem(toolItem, empty); + if (empty) { + setGraphic(null); + setText(null); + return; + } + // null value means this is a group header row — delegate to the Label graphic set in createGroupItem(). + if (toolItem == null) { + TreeItem treeItem = getTreeItem(); + if (isTopLevelGroupHeader(treeItem)) { + setGraphic(treeItem.getGraphic()); + } else { + setGraphic(null); + } + setText(null); + return; + } + + root.getChildren().clear(); + + enabled = createEnabledToggle(toolItem); + name = createToolNameLabel(toolItem); + errorIcon = createErrorIcon(); + edition = createEditionSelector(toolItem); + version = createVersionSelector(toolItem); + + attachVersionValidation(toolItem, errorIcon); + + // Reapply error state if this tool has a validation error + if (controller.validationErrors.contains(toolItem.getToolName())) { + version.setStyle("-fx-font-size: 12; -fx-border-color: red; -fx-border-width: 2;"); + errorIcon.setVisible(true); + errorIcon.setManaged(true); + } + + HBox versionWithIcon = new HBox(5); + versionWithIcon.setAlignment(Pos.CENTER_LEFT); + versionWithIcon.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(version, Priority.ALWAYS); + versionWithIcon.getChildren().addAll(version, errorIcon); + HBox.setHgrow(versionWithIcon, Priority.ALWAYS); + + root.getChildren().addAll(enabled, name, edition, versionWithIcon); + applyEnabledState(toolItem); + setGraphic(root); + } + + // Tree depth: invisible root → group items (depth 1) → tool items (depth 2). + // Group headers are at depth 1: they have a parent (root) but that parent has no parent. + private boolean isTopLevelGroupHeader(TreeItem treeItem) { + return treeItem != null && treeItem.getParent() != null && treeItem.getParent().getParent() == null; + } + + private CheckBox createEnabledToggle(ToolConfiguration toolItem) { + CheckBox enabledToggle = new CheckBox(); + enabledToggle.setPrefWidth(40); + enabledToggle.setSelected(toolItem.isEnabled()); + enabledToggle.setOnAction(_ -> { + toolItem.setEnabled(enabledToggle.isSelected()); + applyEnabledState(toolItem); + }); + return enabledToggle; + } + + private Label createToolNameLabel(ToolConfiguration toolItem) { + Label toolNameLabel = new Label(toolItem.getToolName()); + toolNameLabel.setPrefWidth(120); + toolNameLabel.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(toolNameLabel, Priority.ALWAYS); + toolNameLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); + return toolNameLabel; + } + + private ComboBox createEditionSelector(ToolConfiguration toolItem) { + ComboBox editionSelector = new ComboBox<>(); + editionSelector.setPrefWidth(130); + editionSelector.setMaxWidth(Double.MAX_VALUE); + editionSelector.setPrefHeight(32); + HBox.setHgrow(editionSelector, Priority.ALWAYS); + editionSelector.setEditable(true); + editionSelector.setStyle("-fx-font-size: 12;"); + + List editions = toolItem.getAvailableEditions(); + boolean supportsEdition = toolItem.isSupportsEdition() && editions != null && !editions.isEmpty(); + if (supportsEdition) { + editionSelector.setItems(FXCollections.observableArrayList(editions)); + editionSelector.setValue(toolItem.getConfiguredEdition() == null ? "" : toolItem.getConfiguredEdition()); + editionSelector.setVisible(true); + editionSelector.setManaged(true); + // When the edition changes, reload the version list and re-validate the already-entered version against it + // (a version valid for the old edition may not be for the new one). Version/errorIcon are captured before + // the lambda to avoid referencing the mutable fields from a background thread. + editionSelector.setOnAction(e -> { + String selectedEdition = editionSelector.getValue(); + if (selectedEdition == null || selectedEdition.isBlank() || controller.currentContext == null) { + return; + } + toolItem.setConfiguredEdition(selectedEdition); + ComboBox capturedVersion = version; + Label capturedErrorIcon = errorIcon; + String errorKey = toolItem.getToolName(); + + String enteredVersion = capturedVersion.getValue(); + loadVersions(toolItem, versions -> { + capturedVersion.setItems(FXCollections.observableArrayList(versions)); + capturedVersion.setValue(enteredVersion); + toolItem.setConfiguredVersion(enteredVersion); + applyValidation(errorKey, capturedVersion, capturedErrorIcon, versions); + }); + }); + } else { + editionSelector.setVisible(false); + editionSelector.setManaged(true); + editionSelector.setDisable(true); + } + return editionSelector; + } + + private ComboBox createVersionSelector(ToolConfiguration toolItem) { + ComboBox versionSelector = new ComboBox<>(); + versionSelector.setPrefWidth(130); + versionSelector.setMaxWidth(Double.MAX_VALUE); + versionSelector.setPrefHeight(32); + versionSelector.setEditable(true); + versionSelector.setValue(toolItem.getConfiguredVersion() == null ? "" : toolItem.getConfiguredVersion()); + versionSelector.setStyle("-fx-font-size: 12;"); + + // Lazy-load versions the first time the dropdown is opened to avoid fetching all tools' versions upfront. + // Reuse the list if it was already loaded (e.g. by focus-lost validation) instead of fetching again. + versionSelector.setOnShowing(e -> { + if (!versionSelector.getItems().isEmpty()) { + return; + } + List cachedVersions = toolItem.getAvailableVersions(); + if (cachedVersions != null) { + versionSelector.setItems(FXCollections.observableArrayList(cachedVersions)); + return; + } + loadVersions(toolItem, versions -> { + versionSelector.setItems(FXCollections.observableArrayList(versions)); + // JavaFX doesn't repaint an already-open popup after its items change; + // hide/show forces a fresh layout with the newly loaded list. + if (versionSelector.isShowing()) { + versionSelector.hide(); + versionSelector.show(); + } + }); + }); + + return versionSelector; + } + + /** + * Loads {@code toolItem}'s available versions for its currently configured edition on a background thread, caches them onto {@code toolItem}, then runs + * {@code onLoaded} with the result on the JavaFX application thread. Used by the version dropdown, focus-lost validation, and edition changes. + */ + private void loadVersions(ToolConfiguration toolItem, Consumer> onLoaded) { + if (controller.currentContext == null) { + return; + } + String edition = toolItem.getConfiguredEdition(); + IdeGuiContext loadContext = controller.currentContext; + Thread t = new Thread(() -> { + List versions = controller.service.loadVersionsForSelectedEdition(toolItem.getToolName(), edition, loadContext); + toolItem.setAvailableVersions(versions); + Platform.runLater(() -> onLoaded.accept(versions)); + }); + t.setDaemon(true); + t.start(); + } + + private Label createErrorIcon() { + Label errorIcon = new Label("✗"); + errorIcon.setStyle("-fx-text-fill: red; -fx-font-size: 14; -fx-font-weight: bold; -fx-cursor: hand;"); + errorIcon.setPrefWidth(20); + errorIcon.setVisible(false); + errorIcon.setManaged(false); + + Tooltip errorTooltip = new Tooltip(controller.getNlsService().get("invalidVersionError")); + errorIcon.setOnMouseEntered(e -> { + if (errorIcon.isVisible()) { + errorTooltip.show(errorIcon, e.getScreenX() + 10, e.getScreenY() + 10); + } + }); + errorIcon.setOnMouseExited(e -> errorTooltip.hide()); + return errorIcon; + } + + // Validate free-text version input on focus-lost, and handle setting the version on the ToolConfiguration. + // Blank input is normalized to "*" (meaning "latest") so the field is never left empty. + // If versions were never loaded (dropdown never opened), load them first so typing a value and tabbing/clicking + // away still validates correctly. + private void attachVersionValidation(ToolConfiguration toolItem, Label errorIcon) { + String errorKey = toolItem.getToolName(); + version.focusedProperty().addListener((obs, oldVal, newVal) -> { + if (newVal) { + return; + } + if (version.getValue() == null || version.getValue().isBlank()) { + version.setValue("*"); + } + toolItem.setConfiguredVersion(version.getValue()); + + List availableVersions = toolItem.getAvailableVersions(); + if (availableVersions != null) { + applyValidation(errorKey, version, errorIcon, availableVersions); + } else { + loadVersions(toolItem, versions -> applyValidation(errorKey, version, errorIcon, versions)); + } + }); + } + + /** + * Validates {@code versionSelector}'s current value (using {@link ToolSettingsService#isValidVersion}) against {@code availableVersions} and updates the + * border, error icon, the shared {@code validationErrors} set, and the Save/Preview button state accordingly. + */ + private void applyValidation(String errorKey, ComboBox versionSelector, Label errorIconLabel, List availableVersions) { + boolean valid = controller.service.isValidVersion(versionSelector.getValue(), availableVersions); + if (valid) { + versionSelector.setStyle("-fx-font-size: 12;"); + errorIconLabel.setVisible(false); + errorIconLabel.setManaged(false); + controller.validationErrors.remove(errorKey); + } else { + versionSelector.setStyle("-fx-font-size: 12; -fx-border-color: red; -fx-border-width: 2;"); + errorIconLabel.setVisible(true); + errorIconLabel.setManaged(true); + controller.validationErrors.add(errorKey); + } + controller.updateButtonStates(); + } + + private void applyEnabledState(ToolConfiguration toolItem) { + double opacity = toolItem.isEnabled() ? 1.0 : 0.6; + enabled.setOpacity(opacity); + name.setOpacity(opacity); + version.setOpacity(opacity); + edition.setOpacity(opacity); + version.setDisable(!toolItem.isEnabled()); + edition.setDisable(!toolItem.isEnabled()); + } + } +} + diff --git a/gui/src/main/java/com/devonfw/ide/gui/settings/ToolSettingsService.java b/gui/src/main/java/com/devonfw/ide/gui/settings/ToolSettingsService.java new file mode 100644 index 0000000000..598bb1af24 --- /dev/null +++ b/gui/src/main/java/com/devonfw/ide/gui/settings/ToolSettingsService.java @@ -0,0 +1,245 @@ +package com.devonfw.ide.gui.settings; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.ide.gui.context.IdeGuiContext; +import com.devonfw.tools.ide.commandlet.Commandlet; +import com.devonfw.tools.ide.commandlet.CommandletManager; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.environment.EnvironmentVariables; +import com.devonfw.tools.ide.environment.EnvironmentVariablesType; +import com.devonfw.tools.ide.tool.GlobalToolCommandlet; +import com.devonfw.tools.ide.tool.LocalToolCommandlet; +import com.devonfw.tools.ide.tool.ToolCommandlet; +import com.devonfw.tools.ide.tool.ide.IdeToolCommandlet; +import com.devonfw.tools.ide.tool.npm.NpmBasedCommandlet; +import com.devonfw.tools.ide.tool.pip.PipBasedCommandlet; +import com.devonfw.tools.ide.variable.IdeVariables; +import com.devonfw.tools.ide.version.VersionIdentifier; + +/** + * Service that exposes tool configuration (read/write) based on the existing environment variables machinery. + *

+ * - Reads IDE_TOOLS to detect enabled tools - Reads per-tool _VERSION and _EDITION variables - Writes changes into the settings variables and saves them + */ +public final class ToolSettingsService { + + private static final Logger LOG = LoggerFactory.getLogger(ToolSettingsService.class); + + /** + * Retrieve a list of ToolConfiguration objects for all locally supported tools discovered via the {@link CommandletManager} of the provided + * {@link IdeGuiContext}. + */ + public List listToolConfigurations(IdeGuiContext guiContext) { + List enabledTools = IdeVariables.IDE_TOOLS.get(guiContext); + + List toolConfigurations = new ArrayList<>(); + for (Commandlet commandlet : guiContext.getCommandletManager().getCommandlets()) { + if (commandlet instanceof ToolCommandlet toolCmd) { + ToolConfiguration toolConfiguration = toToolConfiguration(toolCmd, enabledTools); + toolConfiguration.setGroup(determineGroup(toolCmd)); + toolConfigurations.add(toolConfiguration); + } + } + + toolConfigurations.sort(Comparator.comparing((ToolConfiguration tc) -> tc.getGroup() == null ? ToolConfiguration.ToolGroup.OTHER : tc.getGroup()) + .thenComparing(ToolConfiguration::getToolName, String.CASE_INSENSITIVE_ORDER)); + return toolConfigurations; + } + + private ToolConfiguration.ToolGroup determineGroup(ToolCommandlet toolCmd) { + if (toolCmd instanceof IdeToolCommandlet) { + return ToolConfiguration.ToolGroup.IDE; + } else if (toolCmd instanceof PipBasedCommandlet) { + return ToolConfiguration.ToolGroup.PIP; + } else if (toolCmd instanceof NpmBasedCommandlet) { + return ToolConfiguration.ToolGroup.NPM; + } else if (toolCmd instanceof GlobalToolCommandlet) { + return ToolConfiguration.ToolGroup.GLOBAL; + } else if (toolCmd instanceof LocalToolCommandlet) { + return ToolConfiguration.ToolGroup.LOCAL; + } + return ToolConfiguration.ToolGroup.OTHER; + } + + ToolConfiguration toToolConfiguration(ToolCommandlet commandlet, List enabledTools) { + String toolName = commandlet.getName(); + ToolConfiguration toolConfiguration = new ToolConfiguration(toolName); + toolConfiguration.setConfiguredVersion(toConfiguredVersion(commandlet.getConfiguredVersion())); + toolConfiguration.setConfiguredEdition(commandlet.getConfiguredEdition()); + toolConfiguration.setEnabled(isEnabledTool(toolName, enabledTools)); + + return toolConfiguration; + } + + private String toConfiguredVersion(VersionIdentifier version) { + return version == null ? null : version.toString(); + } + + private boolean isEnabledTool(String toolName, List enabledTools) { + return enabledTools != null && enabledTools.stream().anyMatch(name -> name.equalsIgnoreCase(toolName)); + } + + + /** + * Build the ide.properties content that would result from applying the given configurations. This is a plain textual preview and does not attempt to preserve + * comments or formatting. + * + * @param configs the tool configurations + * @return the generated properties content + */ + public String buildPreviewSettingsContent(List configs) { + StringBuilder sb = new StringBuilder(); + List enabledToolNames = getEnabledToolNames(configs); + sb.append(IdeVariables.IDE_TOOLS.getName()).append("=").append(String.join(", ", enabledToolNames)).append(System.lineSeparator()); + sb.append(System.lineSeparator()); + for (ToolConfiguration toolConfiguration : configs) { + if (!toolConfiguration.isEnabled()) { + continue; + } + appendToolVariables(sb, toolConfiguration); + } + return sb.toString(); + } + + private List getEnabledToolNames(List configs) { + return configs.stream().filter(ToolConfiguration::isEnabled).map(tc -> tc.getToolName().toLowerCase(Locale.ROOT)).collect(Collectors.toList()); + } + + private void appendToolVariables(StringBuilder sb, ToolConfiguration toolConfiguration) { + String tool = toolConfiguration.getToolName(); + String versionVar = EnvironmentVariables.getToolVersionVariable(tool); + String editionVar = EnvironmentVariables.getToolEditionVariable(tool); + String version = toolConfiguration.getConfiguredVersion(); + String edition = toolConfiguration.getConfiguredEdition(); + if (version != null && !version.isBlank()) { + sb.append(versionVar).append("=").append(version).append(System.lineSeparator()); + } + if (toolConfiguration.isSupportsEdition()) { + if (edition != null && !edition.isBlank()) { + sb.append(editionVar).append("=").append(edition).append(System.lineSeparator()); + } + } + } + + /** + * Reload available versions for a tool when the edition changes. This is called from the UI when the user selects/changes an edition. + * + * @param tool the tool name + * @param context the current IDE context + * @return the list of available edition for that tool + */ + public List loadEditionsForTool(String tool, IdeContext context) { + try { + ToolCommandlet cmd = context.getCommandletManager().getToolCommandlet(tool); + if (cmd == null) { + return List.of(); + } + return cmd.getToolRepository().getSortedEditions(tool); + } catch (Exception e) { + LOG.debug("Failed to load editions for tool {}: {}", tool, e.getMessage(), e); + return List.of(); + } + } + + /** + * Reload available versions for a tool when the edition changes. This is called from the UI when the user selects/changes an edition. + * + * @param tool the tool name + * @param selectedEdition the newly selected edition + * @param context the current IDE context + * @return the list of available versions for that edition, or empty list on error + */ + public List loadVersionsForSelectedEdition(String tool, String selectedEdition, IdeContext context) { + try { + ToolCommandlet toolCmd = context.getCommandletManager().getToolCommandlet(tool); + + if (toolCmd == null) { + return List.of(); + } + if (selectedEdition == null || selectedEdition.isBlank()) { + return List.of(); + } + List versionIds = toolCmd.getToolRepository().getSortedVersions(tool, selectedEdition, toolCmd); + return versionIds.stream().map(VersionIdentifier::toString).collect(Collectors.toList()); + } catch (Exception e) { + LOG.debug("Failed to load versions for tool {}: {}", tool, e.getMessage(), e); + return List.of(); + } + } + + /** + * Validate a user-entered version against a tool's loaded available versions. + *

+ * Exact versions are checked for literal presence in {@code availableVersions}. Version patterns (e.g. {@code 2026.1*} or {@code 2026*!}, see + * {@link VersionIdentifier#isPattern()}) are considered valid if they {@link VersionIdentifier#matches(VersionIdentifier) match} at least one available + * version. Blank input and {@code "*"} are always valid, and validation is skipped (treated as valid) when {@code availableVersions} has not been loaded + * yet. + * + * @param enteredVersion the version (or version pattern) typed by the user. + * @param availableVersions the available versions for the tool/edition, or {@code null} if not loaded yet. + * @return {@code true} if the entered version is valid, {@code false} otherwise. + */ + public boolean isValidVersion(String enteredVersion, List availableVersions) { + if (enteredVersion == null || enteredVersion.isBlank() || enteredVersion.equals("*")) { + return true; + } + if (availableVersions == null) { + return true; + } + VersionIdentifier entered = VersionIdentifier.of(enteredVersion); + if (entered == null) { + return false; + } + if (entered.isPattern()) { + return availableVersions.stream().map(VersionIdentifier::of).anyMatch(entered::matches); + } + return availableVersions.contains(enteredVersion); + } + + /** + * Apply and persist the provided tool configurations into the settings (ide.properties) file. + *

+ * This method performs a batch update: - updates IDE_TOOLS list - updates each tool's _VERSION and _EDITION variables (in the settings variables layer) + */ + public void applyAndSave(List toolConfigurations, IdeGuiContext guiContext) { + EnvironmentVariables settingsVars = guiContext.getVariables().getByType(EnvironmentVariablesType.SETTINGS); + + settingsVars.set(IdeVariables.IDE_TOOLS.getName(), String.join(", ", getEnabledToolNames(toolConfigurations)), false); + + for (ToolConfiguration toolConfiguration : toolConfigurations) { + String versionVar = EnvironmentVariables.getToolVersionVariable(toolConfiguration.getToolName()); + String editionVar = EnvironmentVariables.getToolEditionVariable(toolConfiguration.getToolName()); + if (toolConfiguration.isEnabled()) { + settingsVars.set(versionVar, emptyToNull(toolConfiguration.getConfiguredVersion()), false); + if (toolConfiguration.isSupportsEdition()) { + settingsVars.set(editionVar, emptyToNull(toolConfiguration.getConfiguredEdition()), false); + } + } else { + settingsVars.set(versionVar, null, false); + if (toolConfiguration.isSupportsEdition()) { + settingsVars.set(editionVar, null, false); + } + } + } + + settingsVars.save(); + } + + private static String emptyToNull(String s) { + if (s == null) { + return null; + } + String t = s.trim(); + return t.isEmpty() ? null : t; + } + +} + 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 348a398d29..a67c10372c 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 @@ -5,7 +5,7 @@ - @@ -61,116 +61,132 @@

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + +
diff --git a/gui/src/main/resources/com/devonfw/ide/gui/style/tabs.css b/gui/src/main/resources/com/devonfw/ide/gui/style/tabs.css new file mode 100644 index 0000000000..3c504478cb --- /dev/null +++ b/gui/src/main/resources/com/devonfw/ide/gui/style/tabs.css @@ -0,0 +1,50 @@ +/* Tab bar background — light gray with a solid bottom border as divider */ +.tab-pane > .tab-header-area > .tab-header-background { + -fx-background-color: #f0f0f0; + -fx-border-color: transparent transparent #c8c8c8 transparent; + -fx-border-width: 0 0 2 0; +} + +/* Content area background */ +.tab-pane > .tab-content-area { + -fx-background-color: #ffffff; +} + +/* Individual tab: more padding, rounded top corners */ +.tab-pane > .tab-header-area > .headers-region > .tab { + -fx-background-color: transparent; + -fx-background-radius: 4 4 0 0; + -fx-background-insets: 0; + -fx-padding: 9 22 9 22; + -fx-cursor: hand; +} + +/* Hover state */ +.tab-pane > .tab-header-area > .headers-region > .tab:hover { + -fx-background-color: #e2e2e2; +} + +/* Selected tab: white background, top+side border, bottom matches content */ +.tab-pane > .tab-header-area > .headers-region > .tab:selected { + -fx-background-color: #ffffff; + -fx-border-color: #c8c8c8 #c8c8c8 #ffffff #c8c8c8; + -fx-border-width: 1; + -fx-border-radius: 4 4 0 0; +} + +/* Tab label: muted for unselected, bold+dark for selected */ +.tab .tab-label { + -fx-font-size: 13px; + -fx-font-family: Arial; + -fx-text-fill: #666666; +} + +.tab:selected .tab-label { + -fx-font-weight: bold; + -fx-text-fill: #1a1a1a; +} + +/* Disabled tab: visually dimmed */ +.tab:disabled .tab-label { + -fx-text-fill: #b0b0b0; +} diff --git a/gui/src/main/resources/com/devonfw/ide/gui/tools-config.fxml b/gui/src/main/resources/com/devonfw/ide/gui/tools-config.fxml new file mode 100644 index 0000000000..99f9d97e5b --- /dev/null +++ b/gui/src/main/resources/com/devonfw/ide/gui/tools-config.fxml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + +
+ + + + + + +