diff --git a/.gitignore b/.gitignore index 59f9c835..77c687e0 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ bin/ !**/src/main/**/bin/ !**/src/test/**/bin/ +### Railroad ### +.railroad/ + ### NetBeans ### /nbproject/private/ /nbbuild/ @@ -47,4 +50,4 @@ bin/ *.class ### Logs ### -hs_err_pid*.log \ No newline at end of file +hs_err_pid*.log diff --git a/META-INF/gradle-plugins/dev.railroadide.railroadgradleplugin.properties b/META-INF/gradle-plugins/dev.railroadide.railroadgradleplugin.properties new file mode 100644 index 00000000..230df5dd --- /dev/null +++ b/META-INF/gradle-plugins/dev.railroadide.railroadgradleplugin.properties @@ -0,0 +1 @@ +implementation-class=dev.railroadide.railroadplugin.RailroadProjectPlugin diff --git a/build.gradle b/build.gradle index 5ff1acb1..04765454 100644 --- a/build.gradle +++ b/build.gradle @@ -89,8 +89,7 @@ dependencies { implementation 'org.apache.maven:maven-repository-metadata:4.0.0-rc-4' implementation 'org.codehaus.plexus:plexus-container-default:2.1.1' - implementation 'dev.railroadide:java-version-extractor-plugin:1.0.0' - implementation 'dev.railroadide:fabric-extractor-plugin:1.0.0' + implementation 'dev.railroadide:RailroadGradlePlugin:1.0.0' implementation 'io.get-coursier:interface:1.0.29-M1' implementation 'org.eclipse.jgit:org.eclipse.jgit:7.4.0.202509020913-r' diff --git a/railroad-core/src/main/java/dev/railroadide/core/ui/RRButton.java b/railroad-core/src/main/java/dev/railroadide/core/ui/RRButton.java index 78297561..924649a5 100644 --- a/railroad-core/src/main/java/dev/railroadide/core/ui/RRButton.java +++ b/railroad-core/src/main/java/dev/railroadide/core/ui/RRButton.java @@ -23,8 +23,7 @@ * Supports different sizes, styles, and icon integration. */ public class RRButton extends Button { - - public static final String[] DEFAULT_STYLE_CLASSES = { "rr-button", "button" }; + public static final String[] DEFAULT_STYLE_CLASSES = {"rr-button", "button"}; private FontIcon icon; @@ -32,10 +31,11 @@ public class RRButton extends Button { private FontIcon loadingSpinner; private final BooleanProperty isLoading = new SimpleBooleanProperty(this, "isLoading", false); + public boolean getIsLoading() { return isLoading.get(); } - + private final LocalizedTextProperty localizedText = new LocalizedTextProperty(this, "localizedText", null); private final BooleanProperty isSquare = new SimpleBooleanProperty(this, "isSquare", false); @@ -124,7 +124,7 @@ public static RRButton warning(String text) { protected void initialize(String localizationKey, Object... args) { getStyleClass().setAll(RRButton.DEFAULT_STYLE_CLASSES); - + setAlignment(Pos.CENTER); setPadding(new Insets(8, 16, 8, 16)); diff --git a/railroad-core/src/main/java/dev/railroadide/core/ui/RRToggleButton.java b/railroad-core/src/main/java/dev/railroadide/core/ui/RRToggleButton.java index 94b4a6ae..85ca5044 100644 --- a/railroad-core/src/main/java/dev/railroadide/core/ui/RRToggleButton.java +++ b/railroad-core/src/main/java/dev/railroadide/core/ui/RRToggleButton.java @@ -4,15 +4,20 @@ import org.kordamp.ikonli.fontawesome6.FontAwesomeSolid; import org.kordamp.ikonli.javafx.FontIcon; -import dev.railroadide.core.ui.localized.LocalizedTextProperty; -import javafx.animation.ScaleTransition; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.ToggleButton; -import javafx.util.Duration; +import dev.railroadide.core.ui.localized.LocalizedTextProperty; +import dev.railroadide.core.ui.styling.ButtonSize; +import dev.railroadide.core.ui.styling.ButtonVariant; +import javafx.animation.ScaleTransition; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.ToggleButton; +import javafx.util.Duration; public class RRToggleButton extends ToggleButton { @@ -28,7 +33,12 @@ public boolean getIsLoading() { return isLoading.get(); } - private final LocalizedTextProperty localizedText = new LocalizedTextProperty(this, "localizedText", null); + private final LocalizedTextProperty localizedText = new LocalizedTextProperty(this, "localizedText", null); + private final BooleanProperty isSquare = new SimpleBooleanProperty(this, "isSquare", false); + private final BooleanProperty isOutlined = new SimpleBooleanProperty(this, "isOutlined", false); + private final BooleanProperty isFlat = new SimpleBooleanProperty(this, "isFlat", false); + private final ObjectProperty variant = new SimpleObjectProperty<>(this, "variant", ButtonVariant.PRIMARY); + private final ObjectProperty size = new SimpleObjectProperty<>(this, "size", ButtonSize.MEDIUM); public RRToggleButton() { this(""); @@ -48,16 +58,71 @@ public RRToggleButton(String localizationKey, Node graphic, Object... args) { setGraphic(graphic); } - public RRToggleButton(String localizationKey, Object... args) { - super(); - - initialize(localizationKey, args); - } - - protected void initialize(String localizationKey, Object... args) { - getStyleClass().setAll(RRToggleButton.DEFAULT_STYLE_CLASSES); - - setPadding(new Insets(8, 16, 8, 16)); + public RRToggleButton(String localizationKey, Object... args) { + super(); + + initialize(localizationKey, args); + } + + /** + * Create a primary toggle button + */ + public static RRToggleButton primary(String text) { + var button = new RRToggleButton(text); + button.setVariant(ButtonVariant.PRIMARY); + return button; + } + + /** + * Create a secondary toggle button + */ + public static RRToggleButton secondary(String text) { + var button = new RRToggleButton(text); + button.setVariant(ButtonVariant.SECONDARY); + return button; + } + + /** + * Create a ghost toggle button + */ + public static RRToggleButton ghost(String text) { + var button = new RRToggleButton(text); + button.setVariant(ButtonVariant.GHOST); + return button; + } + + /** + * Create a danger toggle button + */ + public static RRToggleButton danger(String text) { + var button = new RRToggleButton(text); + button.setVariant(ButtonVariant.DANGER); + return button; + } + + /** + * Create a success toggle button + */ + public static RRToggleButton success(String text) { + var button = new RRToggleButton(text); + button.setVariant(ButtonVariant.SUCCESS); + return button; + } + + /** + * Create a warning toggle button + */ + public static RRToggleButton warning(String text) { + var button = new RRToggleButton(text); + button.setVariant(ButtonVariant.WARNING); + return button; + } + + protected void initialize(String localizationKey, Object... args) { + getStyleClass().setAll(RRToggleButton.DEFAULT_STYLE_CLASSES); + + setAlignment(Pos.CENTER); + setPadding(new Insets(8, 16, 8, 16)); textProperty().bindBidirectional(localizedText); localizedText.setTranslation(localizationKey, args); @@ -84,16 +149,23 @@ protected void initialize(String localizationKey, Object... args) { } }); - isLoading.addListener($ -> { - if (getIsLoading()) { - onLoading(); - } else { - onNotLoading(); - } - }); - - updateContent(); - } + isLoading.addListener($ -> { + if (getIsLoading()) { + onLoading(); + } else { + onNotLoading(); + } + }); + + variant.addListener($ -> updateStyle()); + size.addListener($ -> updateStyle()); + isSquare.addListener($ -> updateStyle()); + isOutlined.addListener($ -> updateStyle()); + isFlat.addListener($ -> updateStyle()); + + updateStyle(); + updateContent(); + } /** * Set the button text using a localization key with optional formatting arguments. @@ -102,14 +174,28 @@ protected void initialize(String localizationKey, Object... args) { * @param localizationKey the localization key for the text * @param args optional formatting arguments for the localized text */ - public void setLocalizedText(String localizationKey, Object... args) { - localizedText.setTranslation(localizationKey, args); - } - - /** - * Set an icon for the button - */ - public void setIcon(Ikon iconCode) { + public void setLocalizedText(String localizationKey, Object... args) { + localizedText.setTranslation(localizationKey, args); + } + + /** + * Set the button variant + */ + public void setVariant(ButtonVariant variant) { + this.variant.set(variant); + } + + /** + * Set the button size + */ + public void setButtonSize(ButtonSize size) { + this.size.set(size); + } + + /** + * Set an icon for the button + */ + public void setIcon(Ikon iconCode) { if (icon != null && getGraphic() == icon) { setGraphic(null); } @@ -127,13 +213,71 @@ public void setIcon(Ikon iconCode) { } } - /** - * Set loading state for the button - */ - public void setLoading(boolean loading) { - isLoading.set(loading); - } - + /** + * Set loading state for the button. + *

+ * When loading is true: + * - The button becomes disabled and shows a spinning icon + * - The text changes to "Loading..." if there was original text + * - The button gets a "loading" CSS class for styling + * - Click animations are disabled during loading + *

+ * When loading is false: + * - The button is re-enabled and shows the original content + * - Original text and icon are restored + * - The "loading" CSS class is removed + *

+ * Example usage: + *

+     * RRToggleButton button = RRToggleButton.primary("Save");
+     * button.setOnAction(e -> {
+     *     button.setLoading(true);
+     *     // Perform async operation
+     *     CompletableFuture.runAsync(() -> {
+     *         // Do work...
+     *         Platform.runLater(() -> button.setLoading(false));
+     *     });
+     * });
+     * 
+ * + * @param loading true to show loading state, false to restore normal state + */ + public void setLoading(boolean loading) { + isLoading.set(loading); + } + + /** + * Set the button as rounded + */ + public void setRounded(boolean rounded) { + if (rounded) { + getStyleClass().add("rounded"); + } else { + getStyleClass().remove("rounded"); + } + } + + /** + * Force the button into a square shape. + */ + public void setSquare(boolean square) { + isSquare.set(square); + } + + /** + * Set the button as outlined + */ + public void setOutlined(boolean outlined) { + isOutlined.set(outlined); + } + + /** + * Set the button as flat + */ + public void setFlat(boolean flat) { + isFlat.set(flat); + } + /** * Called when the button has started loading */ @@ -187,9 +331,41 @@ private void updateContent() { } else { setGraphic(icon); } - } else { - setGraphic(null); - } - } - -} + } else { + setGraphic(null); + } + } + + private void updateStyle() { + ObservableList styleClass = getStyleClass(); + + styleClass.removeAll("square", "outlined", "flat"); + styleClass.removeAll("primary", "secondary", "ghost", "danger", "success", "warning"); + styleClass.removeAll("small", "medium", "large"); + + if (isSquare.get()) + styleClass.add("square"); + + if (isOutlined.get()) + styleClass.add("outlined"); + + if (isFlat.get()) + styleClass.add("flat"); + + switch (variant.get()) { + case PRIMARY -> styleClass.add("primary"); + case SECONDARY -> styleClass.add("secondary"); + case GHOST -> styleClass.add("ghost"); + case DANGER -> styleClass.add("danger"); + case SUCCESS -> styleClass.add("success"); + case WARNING -> styleClass.add("warning"); + } + + switch (size.get()) { + case SMALL -> styleClass.add("small"); + case MEDIUM -> styleClass.add("medium"); + case LARGE -> styleClass.add("large"); + } + } + +} diff --git a/railroad-core/src/main/java/dev/railroadide/core/ui/localized/LocalizedMenuItem.java b/railroad-core/src/main/java/dev/railroadide/core/ui/localized/LocalizedMenuItem.java index 8494d149..7fc7d383 100644 --- a/railroad-core/src/main/java/dev/railroadide/core/ui/localized/LocalizedMenuItem.java +++ b/railroad-core/src/main/java/dev/railroadide/core/ui/localized/LocalizedMenuItem.java @@ -2,6 +2,7 @@ import dev.railroadide.core.settings.keybinds.KeybindData; import dev.railroadide.core.utility.DesktopUtils; +import javafx.scene.Node; import javafx.scene.control.MenuItem; /** @@ -9,7 +10,7 @@ * It also supports setting a url to open when the item is clicked, additionally allows for a keybind to be set to trigger the items action. */ public class LocalizedMenuItem extends MenuItem { - + private final LocalizedTextProperty localizedText = new LocalizedTextProperty(this, "localizedText", null); /** @@ -23,6 +24,17 @@ public LocalizedMenuItem(final String key) { setKey(key); } + /** + * Creates a new LocalizedMenuItem with the specified key and graphic. + * + * @param key The localization key + * @param graphic The graphic node to display alongside the text + */ + public LocalizedMenuItem(final String key, Node graphic) { + this(key); + setGraphic(graphic); + } + /** * Creates a new LocalizedMenuItem with the specified key and URL. * diff --git a/railroad-core/src/main/java/dev/railroadide/core/ui/localized/LocalizedTab.java b/railroad-core/src/main/java/dev/railroadide/core/ui/localized/LocalizedTab.java new file mode 100644 index 00000000..191f224a --- /dev/null +++ b/railroad-core/src/main/java/dev/railroadide/core/ui/localized/LocalizedTab.java @@ -0,0 +1,51 @@ +package dev.railroadide.core.ui.localized; + +import dev.railroadide.core.localization.LocalizationService; +import dev.railroadide.core.utility.ServiceLocator; +import javafx.scene.Node; +import javafx.scene.control.Tab; + +/** + * An extension of the JavaFX Tab that allows for the Tab's text to be localised. + */ +public class LocalizedTab extends Tab { + private String currentKey; + + public LocalizedTab(String titleKey) { + super(); + setKey(titleKey); + setText(ServiceLocator.getService(LocalizationService.class).get(titleKey)); + } + + public LocalizedTab() { + super(); + } + + public LocalizedTab(String titleKey, Node content) { + this(titleKey); + setContent(content); + } + + /** + * Gets the current key used for localization. + * + * @return The current localization key. + */ + public String getKey() { + return currentKey; + } + + /** + * Sets the key and then updates the text of the label. + * Adds a listener to the current language property to update the text when the language changes. + * + * @param key The localization key + */ + public void setKey(final String key) { + currentKey = key; + LocalizationService l18n = ServiceLocator.getService(LocalizationService.class); + l18n.currentLanguageProperty().addListener((observable, oldValue, newValue) -> + setText(l18n.get(key))); + setText(l18n.get(currentKey)); + } +} diff --git a/src/main/java/dev/railroadide/railroad/DefaultGradleEnvironment.java b/src/main/java/dev/railroadide/railroad/DefaultGradleEnvironment.java index 1b4f36cd..c68a6a42 100644 --- a/src/main/java/dev/railroadide/railroad/DefaultGradleEnvironment.java +++ b/src/main/java/dev/railroadide/railroad/DefaultGradleEnvironment.java @@ -9,7 +9,6 @@ import dev.railroadide.railroad.project.Project; import java.nio.file.Path; -import java.time.Duration; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -18,7 +17,7 @@ public record DefaultGradleEnvironment(Project project, Path gradleInstallationP GradleSettings settings) implements GradleEnvironment { @Override public boolean useWrapper() { - return settings.useWrapper(); + return settings.isUseWrapper(); } @Override @@ -28,17 +27,17 @@ public Optional installationPath() { @Override public Optional userHomePath() { - return Optional.ofNullable(settings.gradleUserHome()); + return Optional.ofNullable(settings.getGradleUserHome()); } @Override public Optional jvm() { - return Optional.ofNullable(settings.gradleJvm()); + return Optional.ofNullable(settings.getGradleJvm()); } @Override public String jvmArgumentsFor(GradleTaskExecutionRequest request, JDK jvm) { - List> configurations = settings.configurations(); + List> configurations = settings.getConfigurations(); if (configurations == null || configurations.isEmpty()) return ""; @@ -63,8 +62,8 @@ public boolean isDaemonEnabled() { } @Override - public Optional daemonIdleTimeout() { - return Optional.ofNullable(settings.daemonIdleTimeout()); + public Optional daemonIdleTimeout() { + return Optional.ofNullable(settings.getDaemonIdleTimeout()); } private boolean matchesConfiguration(GradleTaskExecutionRequest request, diff --git a/src/main/java/dev/railroadide/railroad/gradle/GradleEnvironment.java b/src/main/java/dev/railroadide/railroad/gradle/GradleEnvironment.java index ed633aed..aff4aa43 100644 --- a/src/main/java/dev/railroadide/railroad/gradle/GradleEnvironment.java +++ b/src/main/java/dev/railroadide/railroad/gradle/GradleEnvironment.java @@ -5,7 +5,6 @@ import dev.railroadide.railroad.project.Project; import java.nio.file.Path; -import java.time.Duration; import java.util.Optional; /** @@ -67,7 +66,7 @@ public interface GradleEnvironment { /** * Retrieves the idle timeout duration for the Gradle daemon, if specified. * - * @return an Optional containing the idle timeout duration, or an empty Optional if not set. + * @return an Optional containing the idle timeout in minutes, or an empty Optional if not set. */ - Optional daemonIdleTimeout(); + Optional daemonIdleTimeout(); } diff --git a/src/main/java/dev/railroadide/railroad/gradle/GradleOutputStream.java b/src/main/java/dev/railroadide/railroad/gradle/GradleOutputStream.java index 0333aebc..7b9e67b9 100644 --- a/src/main/java/dev/railroadide/railroad/gradle/GradleOutputStream.java +++ b/src/main/java/dev/railroadide/railroad/gradle/GradleOutputStream.java @@ -3,8 +3,6 @@ import org.jetbrains.annotations.NotNull; import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; import java.util.function.Consumer; /** diff --git a/src/main/java/dev/railroadide/railroad/gradle/GradleSettings.java b/src/main/java/dev/railroadide/railroad/gradle/GradleSettings.java index fd236062..5fbc84cd 100644 --- a/src/main/java/dev/railroadide/railroad/gradle/GradleSettings.java +++ b/src/main/java/dev/railroadide/railroad/gradle/GradleSettings.java @@ -1,32 +1,51 @@ package dev.railroadide.railroad.gradle; -import dev.railroadide.railroad.gradle.service.task.GradleTaskExecutionRequest; import dev.railroadide.railroad.ide.runconfig.RunConfiguration; -import dev.railroadide.railroad.ide.runconfig.defaults.data.GradleRunConfigurationData; import dev.railroadide.railroad.java.JDK; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; import java.nio.file.Path; -import java.time.Duration; import java.util.List; /** * Captures the CLI and project settings that should be applied when invoking Gradle. - * - * @param useWrapper whether to force Gradle to use the project wrapper - * @param wrapperVersion the wrapper version that should be used when {@code useWrapper} is {@code true} - * @param customGradleHome overrides the location of the Gradle distribution to use - * @param gradleUserHome overrides the Gradle user home directory - * @param gradleJvm the JVM definition that Gradle should run under - * @param offlineMode whether Gradle should perform builds without network access - * @param enableBuildCache whether the shared Gradle build cache should be enabled - * @param parallelExecution whether Gradle should allow parallel project execution - * @param maxWorkerCount the maximum number of worker threads Gradle may spawn - * @param configurations run configurations that may influence how Gradle is executed - * @param isDaemonEnabled whether the Gradle daemon should be enabled - * @param daemonIdleTimeout the duration of inactivity after which the Gradle daemon should shut down */ -public record GradleSettings(boolean useWrapper, String wrapperVersion, Path customGradleHome, Path gradleUserHome, - JDK gradleJvm, boolean offlineMode, boolean enableBuildCache, boolean parallelExecution, - int maxWorkerCount, List> configurations, - boolean isDaemonEnabled, Duration daemonIdleTimeout) { +@Getter +@Setter +@ToString +@EqualsAndHashCode +public class GradleSettings { + private boolean useWrapper; + private String wrapperVersion; + private Path customGradleHome; + private Path gradleUserHome; + private JDK gradleJvm; + private boolean offlineMode; + private boolean enableBuildCache; + private boolean parallelExecution; + private int maxWorkerCount; + private List> configurations; + private boolean daemonEnabled; + private Long daemonIdleTimeout; + + public GradleSettings(boolean useWrapper, String wrapperVersion, Path customGradleHome, Path gradleUserHome, + JDK gradleJvm, boolean offlineMode, boolean enableBuildCache, boolean parallelExecution, + int maxWorkerCount, List> configurations, + boolean daemonEnabled, Long daemonIdleTimeout) { + this.useWrapper = useWrapper; + this.wrapperVersion = wrapperVersion; + this.customGradleHome = customGradleHome; + this.gradleUserHome = gradleUserHome; + this.gradleJvm = gradleJvm; + this.offlineMode = offlineMode; + this.enableBuildCache = enableBuildCache; + this.parallelExecution = parallelExecution; + this.maxWorkerCount = maxWorkerCount; + this.configurations = configurations; + this.daemonEnabled = daemonEnabled; + this.daemonIdleTimeout = daemonIdleTimeout; + } } diff --git a/src/main/java/dev/railroadide/railroad/gradle/model/GradleBuildModel.java b/src/main/java/dev/railroadide/railroad/gradle/model/GradleBuildModel.java index 7c77c6cc..5e8f5daa 100644 --- a/src/main/java/dev/railroadide/railroad/gradle/model/GradleBuildModel.java +++ b/src/main/java/dev/railroadide/railroad/gradle/model/GradleBuildModel.java @@ -1,14 +1,18 @@ package dev.railroadide.railroad.gradle.model; +import dev.railroadide.railroadplugin.dto.FabricDataModel; +import dev.railroadide.railroadplugin.dto.RailroadProject; + import java.nio.file.Path; -import java.util.List; /** * Represents a Gradle build and its constituent projects. * * @param gradleVersion the version of Gradle used to build the project * @param rootDir the root directory of the imported build - * @param projects the list of projects contained in the build + * @param fabricData the Fabric modding platform data model, if applicable + * @param project the root project of the Gradle build */ -public record GradleBuildModel(String gradleVersion, Path rootDir, List projects) { +public record GradleBuildModel(String gradleVersion, Path rootDir, FabricDataModel fabricData, + RailroadProject project) { } diff --git a/src/main/java/dev/railroadide/railroad/gradle/model/GradleModelMapper.java b/src/main/java/dev/railroadide/railroad/gradle/model/GradleModelMapper.java deleted file mode 100644 index faa68f56..00000000 --- a/src/main/java/dev/railroadide/railroad/gradle/model/GradleModelMapper.java +++ /dev/null @@ -1,103 +0,0 @@ -package dev.railroadide.railroad.gradle.model; - -import dev.railroadide.railroad.gradle.model.task.GradleTaskArgument; -import dev.railroadide.railroad.gradle.model.task.GradleTaskModel; -import org.gradle.tooling.model.GradleProject; -import org.gradle.tooling.model.GradleTask; - -import java.util.*; - -/** - * Utility class for mapping Gradle project and task models. - */ -public class GradleModelMapper { - - /** - * Maps a Gradle project to a GradleProjectModel without task arguments. - * - * @param project the Gradle project to map. - * @return the mapped GradleProjectModel. - */ - public static GradleProjectModel mapProject(GradleProject project) { - return mapProject(project, Collections.emptyMap()); - } - - /** - * Maps a Gradle project to a GradleProjectModel, including task arguments. - * - * @param project the Gradle project to map. - * @param argumentsByPath a map of task paths to their respective arguments. - * @return the mapped GradleProjectModel. - */ - public static GradleProjectModel mapProject(GradleProject project, Map> argumentsByPath) { - List tasks = new ArrayList<>(); - for (GradleTask task : project.getTasks()) { - tasks.add(mapTask(task, argumentsByPath)); - } - - return new GradleProjectModel( - project.getPath(), - project.getName(), - project.getProjectDirectory().toPath(), - tasks - ); - } - - /** - * Collects Gradle projects into a list of GradleProjectModel without task arguments. - * - * @param project the root Gradle project. - * @param collected the list to collect the mapped GradleProjectModel instances. - */ - public static void collectProjects(GradleProject project, List collected) { - collectProjects(project, collected, Collections.emptyMap()); - } - - /** - * Collects Gradle projects into a list of GradleProjectModel, including task arguments. - * - * @param project the root Gradle project. - * @param collected the list to collect the mapped GradleProjectModel instances. - * @param argumentsByPath a map of task paths to their respective arguments. - */ - public static void collectProjects(GradleProject project, List collected, Map> argumentsByPath) { - collected.add(mapProject(project, argumentsByPath)); - for (GradleProject child : project.getChildren()) { - collectProjects(child, collected, argumentsByPath); - } - } - - /** - * Maps a Gradle task to a GradleTaskModel, including task arguments. - * - * @param task the Gradle task to map. - * @param argumentsByPath a map of task paths to their respective arguments. - * @return the mapped GradleTaskModel. - */ - public static GradleTaskModel mapTask(GradleTask task, Map> argumentsByPath) { - return new GradleTaskModel( - task.getPath(), - task.getName(), - task.getGroup(), - task.getDescription() != null ? task.getDescription() : "", - task.getGroup() != null ? task.getGroup() : "", - isIncrementalSafe(task), - argumentsByPath.getOrDefault(task.getPath(), List.of()) - ); - } - - /** - * Determines if a Gradle task is incremental-safe. - * A task is considered incremental-safe if its name does not start with "clean". - *

- * TODO: Improve this logic to accurately reflect task incremental safety based on more criteria. - * - * @param task the Gradle task to check. - * @return true if the task is incremental-safe, false otherwise. - */ - private static boolean isIncrementalSafe(GradleTask task) { - String name = task.getName().toLowerCase(Locale.ROOT); - // Very naive: non-clean tasks are assumed to be incremental-safe - return !name.startsWith("clean"); - } -} diff --git a/src/main/java/dev/railroadide/railroad/gradle/model/GradleProjectModel.java b/src/main/java/dev/railroadide/railroad/gradle/model/GradleProjectModel.java index 96bc5347..d3e7cd2f 100644 --- a/src/main/java/dev/railroadide/railroad/gradle/model/GradleProjectModel.java +++ b/src/main/java/dev/railroadide/railroad/gradle/model/GradleProjectModel.java @@ -1,6 +1,7 @@ package dev.railroadide.railroad.gradle.model; -import dev.railroadide.railroad.gradle.model.task.GradleTaskModel; +import dev.railroadide.railroadplugin.dto.RailroadConfiguration; +import dev.railroadide.railroadplugin.dto.RailroadGradleTask; import java.nio.file.Path; import java.util.List; @@ -8,10 +9,13 @@ /** * Captures information about a single Gradle project within a composite build. * - * @param path the colon-separated path identifying the project in the build - * @param name the user-visible name of the project - * @param projectDir the directory where the project resides - * @param tasks the tasks exposed by this project + * @param path the colon-separated path identifying the project in the build + * @param name the user-visible name of the project + * @param projectDir the directory where the project resides + * @param tasks the tasks exposed by this project + * @param configurationTrees the configuration trees for dependencies in this project */ -public record GradleProjectModel(String path, String name, Path projectDir, List tasks) { +public record GradleProjectModel(String path, String name, Path projectDir, + List tasks, + List configurationTrees) { } diff --git a/src/main/java/dev/railroadide/railroad/gradle/model/task/GradleTaskArgType.java b/src/main/java/dev/railroadide/railroad/gradle/model/task/GradleTaskArgType.java deleted file mode 100644 index 24af7d28..00000000 --- a/src/main/java/dev/railroadide/railroad/gradle/model/task/GradleTaskArgType.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.railroadide.railroad.gradle.model.task; - -/** - * Defines how a Gradle task argument value should be interpreted and rendered. - */ -public enum GradleTaskArgType { - STRING, - BOOLEAN, - ENUM, - FILE, - DIRECTORY, - NUMBER -} diff --git a/src/main/java/dev/railroadide/railroad/gradle/model/task/GradleTaskArgument.java b/src/main/java/dev/railroadide/railroad/gradle/model/task/GradleTaskArgument.java deleted file mode 100644 index 195c32ee..00000000 --- a/src/main/java/dev/railroadide/railroad/gradle/model/task/GradleTaskArgument.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.railroadide.railroad.gradle.model.task; - -import java.util.List; - -/** - * Describes a single argument that can be passed to a Gradle task. - * - * @param name the CLI name of the argument - * @param displayName a user-friendly label for the argument - * @param type how the argument value should be handled - * @param defaultValue the default value to use if the argument is omitted - * @param description explanatory text about the argument - * @param enumValues candidate values when {@link GradleTaskArgType#ENUM} is used - */ -public record GradleTaskArgument(String name, String displayName, GradleTaskArgType type, String defaultValue, - String description, List enumValues) { -} diff --git a/src/main/java/dev/railroadide/railroad/gradle/model/task/GradleTaskModel.java b/src/main/java/dev/railroadide/railroad/gradle/model/task/GradleTaskModel.java deleted file mode 100644 index 34a5ed5b..00000000 --- a/src/main/java/dev/railroadide/railroad/gradle/model/task/GradleTaskModel.java +++ /dev/null @@ -1,19 +0,0 @@ -package dev.railroadide.railroad.gradle.model.task; - -import java.util.List; - -/** - * Describes a Gradle task, including metadata required for display and execution. - * - * @param path the full task path (for example {@code :project:task}) - * @param name the human-readable task name - * @param group the Gradle group used to categorize the task - * @param description the task description provided by Gradle - * @param category a user-friendly category for the task - * @param isIncrementalSafe whether the task can run incrementally without side effects - * @param arguments the arguments that Gradle will accept for this task - */ -public record GradleTaskModel(String path, String name, String group, String description, - String category, boolean isIncrementalSafe, - List arguments) { -} diff --git a/src/main/java/dev/railroadide/railroad/gradle/project/GradleInvocationPreferences.java b/src/main/java/dev/railroadide/railroad/gradle/project/GradleInvocationPreferences.java index 594d78cb..7e383872 100644 --- a/src/main/java/dev/railroadide/railroad/gradle/project/GradleInvocationPreferences.java +++ b/src/main/java/dev/railroadide/railroad/gradle/project/GradleInvocationPreferences.java @@ -1,7 +1,6 @@ package dev.railroadide.railroad.gradle.project; import java.nio.file.Path; -import java.time.Duration; /** * Preferences for invoking Gradle builds. @@ -43,11 +42,11 @@ static GradleInvocationPreferences defaults() { } /** - * Returns the daemon idle timeout as a Duration. + * Returns the daemon idle timeout in minutes. * - * @return the daemon idle timeout, or null if not set + * @return the daemon idle timeout in minutes, or null if not set */ - Duration daemonIdleTimeout() { - return daemonIdleTimeoutMinutes == null ? null : Duration.ofMinutes(daemonIdleTimeoutMinutes); + Long daemonIdleTimeout() { + return daemonIdleTimeoutMinutes; } } diff --git a/src/main/java/dev/railroadide/railroad/gradle/project/GradleManager.java b/src/main/java/dev/railroadide/railroad/gradle/project/GradleManager.java index c301a92c..d0725ad7 100644 --- a/src/main/java/dev/railroadide/railroad/gradle/project/GradleManager.java +++ b/src/main/java/dev/railroadide/railroad/gradle/project/GradleManager.java @@ -1,6 +1,8 @@ package dev.railroadide.railroad.gradle.project; +import dev.railroadide.railroad.AppResources; import dev.railroadide.railroad.DefaultGradleEnvironment; +import dev.railroadide.railroad.Railroad; import dev.railroadide.railroad.gradle.GradleEnvironment; import dev.railroadide.railroad.gradle.GradleSettings; import dev.railroadide.railroad.gradle.service.GradleConsoleMode; @@ -15,12 +17,11 @@ import dev.railroadide.railroad.java.JDK; import dev.railroadide.railroad.java.JDKManager; import dev.railroadide.railroad.project.Project; -import dev.railroadide.railroad.project.facet.Facet; -import dev.railroadide.railroad.project.facet.FacetManager; -import dev.railroadide.railroad.project.facet.data.GradleFacetData; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.List; import java.util.Map; import java.util.Objects; @@ -28,11 +29,16 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Encapsulates all Gradle-related state for a {@link Project}, including cached environments and models. */ public final class GradleManager { + private static final String DOWNLOAD_SOURCES_TASK = "railroadDownloadAllSources"; + private static final String DOWNLOAD_SOURCES_INIT_RESOURCE = "scripts/init-download-sources.gradle"; + private final Project project; private final Object lock = new Object(); @@ -41,6 +47,7 @@ public final class GradleManager { private GradleModelService modelService; private GradleExecutionService executionService; private GradleEnvironment environment; + private GradleSettings gradleSettings; /** * Creates a new Gradle manager for the given project. @@ -92,10 +99,7 @@ public GradleEnvironment getGradleEnvironment() { synchronized (lock) { if (environment == null) { - Facet gradleFacet = project.getFacet(FacetManager.GRADLE).orElseThrow(); - GradleFacetData gradleData = gradleFacet.getData(); - - GradleSettings settings = buildGradleSettings(gradleData); + GradleSettings settings = getGradleSettings(); Path gradleHome = discoverGradleInstallationPath(); environment = new DefaultGradleEnvironment( @@ -109,6 +113,18 @@ public GradleEnvironment getGradleEnvironment() { } } + public GradleSettings getGradleSettings() { + ensureIsGradleProject(); + + synchronized (lock) { + if (gradleSettings == null) { + gradleSettings = buildGradleSettings(); + } + + return gradleSettings; + } + } + /** * Runs a simple task using the shared execution service; completes the provided future when done. * @@ -152,16 +168,86 @@ public void runBuildTaskAsync(String taskName, JDK jdk, CompletableFuture downloadAllSources() { + var completion = new CompletableFuture(); + + try { + ensureIsGradleProject(); + } catch (IllegalStateException exception) { + completion.completeExceptionally(exception); + return completion; + } + + Path initScriptPath; + try { + initScriptPath = extractInitScript(DOWNLOAD_SOURCES_INIT_RESOURCE, "railroad-download-sources"); + } catch (IOException exception) { + completion.completeExceptionally(exception); + return completion; + } + + GradleSettings settings = getGradleSettings(); + JDK jdk = settings.getGradleJvm() != null ? settings.getGradleJvm() : JDKManager.getDefaultJDK(); + GradleExecutionService execService = getExecutionService(jdk); + + boolean offline = settings.isOfflineMode(); + var request = new GradleTaskExecutionRequest( + DOWNLOAD_SOURCES_TASK, + List.of("--init-script", initScriptPath.toAbsolutePath().toString()), + Map.of(), + Map.of(), + offline, + !offline, + false, + GradleConsoleMode.RICH + ); + + GradleTaskExecutionHandle handle; + try { + handle = execService.runTask(request); + } catch (Exception exception) { + deleteIfExists(initScriptPath); + completion.completeExceptionally(exception); + return completion; + } + + handle.completionFuture().whenComplete((result, throwable) -> { + deleteIfExists(initScriptPath); + if (throwable != null) { + completion.completeExceptionally(throwable); + } else { + completion.complete(null); + } + }); + + return completion; + } + private void ensureIsGradleProject() { - if (!project.hasFacet(FacetManager.GRADLE)) - throw new IllegalStateException("Project does not have a Gradle facet."); + Path path = project.getPath(); + if (!isGradleProject()) + throw new IllegalStateException("Project at " + path + " is not a Gradle project."); + } + + public boolean isGradleProject() { + Path path = project.getPath(); + Path groovyBuildFile = path.resolve("build.gradle"); + Path kotlinBuildFile = path.resolve("build.gradle.kts"); + boolean hasGroovyBuild = Files.isRegularFile(groovyBuildFile) && Files.isReadable(groovyBuildFile); + boolean hasKotlinBuild = Files.isRegularFile(kotlinBuildFile) && Files.isReadable(kotlinBuildFile); + return hasGradleWrapper() || hasGroovyBuild || hasKotlinBuild; } - private GradleSettings buildGradleSettings(GradleFacetData gradleData) { + private GradleSettings buildGradleSettings() { GradleInvocationPreferences prefs = loadGradleInvocationPreferences(); boolean useWrapper = hasGradleWrapper(); - String wrapperVersion = gradleData != null ? gradleData.getGradleVersion() : null; + String wrapperVersion = getGradleVersion(); Path gradleUserHome = getEnvPath("GRADLE_USER_HOME").orElse(prefs.gradleUserHome()); JDK gradleJvm = JDKManager.getDefaultJDK(); @@ -205,6 +291,41 @@ private boolean hasGradleWrapper() { return Files.isRegularFile(wrapperProps); } + private String getGradleVersion() { + Path wrapperProps = project.getPath().resolve("gradle").resolve("wrapper").resolve("gradle-wrapper.properties"); + if (!Files.isRegularFile(wrapperProps)) + return null; + + try { + for (String rawLine : Files.readAllLines(wrapperProps)) { + String line = rawLine.trim(); + if (line.startsWith("distributionUrl=")) { + String url = line.substring("distributionUrl=".length()).trim(); + + if ((url.startsWith("\"") && url.endsWith("\"")) || (url.startsWith("'") && url.endsWith("'"))) { + url = url.substring(1, url.length() - 1); + } + + int lastSlash = url.lastIndexOf('/'); + String filename = lastSlash != -1 ? url.substring(lastSlash + 1) : url; + + Matcher m = Pattern.compile("gradle-([0-9][0-9A-Za-z.-]*)", Pattern.CASE_INSENSITIVE) + .matcher(filename); + if (m.find()) + return m.group(1); + + // Couldn't extract version from filename + return null; + } + } + + return null; + } catch (IOException exception) { + Railroad.LOGGER.error("Error reading gradle-wrapper.properties", exception); + return null; + } + } + private Optional getEnvPath(String envKey) { String value = System.getenv(envKey); if (value == null || value.isBlank()) @@ -223,6 +344,48 @@ private GradleInvocationPreferences loadGradleInvocationPreferences() { .orElseGet(GradleInvocationPreferences::defaults); } + private Path extractInitScript(String resourcePath, String prefix) throws IOException { + try (var stream = AppResources.getResourceAsStream(resourcePath)) { + if (stream == null) + throw new IOException("Missing init script resource: " + resourcePath); + + Path tempFile = Files.createTempFile(prefix, ".gradle"); + Files.copy(stream, tempFile, StandardCopyOption.REPLACE_EXISTING); + tempFile.toFile().deleteOnExit(); + return tempFile; + } + } + + private void deleteIfExists(Path path) { + if (path == null) + return; + + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + } + } + + public void saveSettings() { + synchronized (lock) { + project.getDataStore().writeJson( + "gradle/settings.json", + new GradleInvocationPreferences( + gradleSettings.isOfflineMode(), + gradleSettings.isEnableBuildCache(), + gradleSettings.isParallelExecution(), + gradleSettings.isDaemonEnabled(), + gradleSettings.getDaemonIdleTimeout(), + gradleSettings.getMaxWorkerCount(), + gradleSettings.getCustomGradleHome(), + gradleSettings.getGradleUserHome() + ) + ); + + this.environment = null; // Invalidate cached environment + } + } + // TODO: Support changing JDK at runtime (this would require recreating the execution service) private GradleExecutionService getExecutionService(JDK jdkOverride) { synchronized (lock) { diff --git a/src/main/java/dev/railroadide/railroad/gradle/project/JdkOverridingEnvironment.java b/src/main/java/dev/railroadide/railroad/gradle/project/JdkOverridingEnvironment.java index 100fc156..53680e89 100644 --- a/src/main/java/dev/railroadide/railroad/gradle/project/JdkOverridingEnvironment.java +++ b/src/main/java/dev/railroadide/railroad/gradle/project/JdkOverridingEnvironment.java @@ -6,7 +6,6 @@ import dev.railroadide.railroad.project.Project; import java.nio.file.Path; -import java.time.Duration; import java.util.Optional; /** @@ -52,7 +51,7 @@ public boolean isDaemonEnabled() { } @Override - public Optional daemonIdleTimeout() { + public Optional daemonIdleTimeout() { return delegate.daemonIdleTimeout(); } } diff --git a/src/main/java/dev/railroadide/railroad/gradle/service/GradleModelService.java b/src/main/java/dev/railroadide/railroad/gradle/service/GradleModelService.java index dd36c45a..7ac5ad90 100644 --- a/src/main/java/dev/railroadide/railroad/gradle/service/GradleModelService.java +++ b/src/main/java/dev/railroadide/railroad/gradle/service/GradleModelService.java @@ -1,9 +1,8 @@ package dev.railroadide.railroad.gradle.service; import dev.railroadide.railroad.gradle.model.GradleBuildModel; -import dev.railroadide.railroad.gradle.model.task.GradleTaskModel; +import dev.railroadide.railroad.gradle.model.GradleModelListener; -import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -26,37 +25,16 @@ public interface GradleModelService { Optional getCachedModel(); /** - * @return every task from the most recently cached model, or an empty list if no model is loaded - */ - default List getAllTasks() { - return getCachedModel().map(GradleBuildModel::projects) - .stream() - .flatMap(projects -> projects.stream() - .flatMap(project -> project.tasks().stream())) - .toList(); - } - - /** - * Finds tasks that match the provided name. + * Adds a listener to be notified of model changes. * - * @param name the task name to search for - * @return tasks whose simple name matches the provided string + * @param listener the listener to add */ - default List findTasksByName(String name) { - return getAllTasks().stream() - .filter(task -> task.name().equals(name)) - .toList(); - } + void addListener(GradleModelListener listener); /** - * Looks up a task by its full Gradle path. + * Removes a previously added listener. * - * @param path the fully-qualified task path - * @return the matching task if it exists + * @param listener the listener to remove */ - default Optional findTaskByPath(String path) { - return getAllTasks().stream() - .filter(task -> task.path().equals(path)) - .findFirst(); - } + void removeListener(GradleModelListener listener); } diff --git a/src/main/java/dev/railroadide/railroad/gradle/service/impl/ToolingGradleExecutionService.java b/src/main/java/dev/railroadide/railroad/gradle/service/impl/ToolingGradleExecutionService.java index 6774616a..01a1618a 100644 --- a/src/main/java/dev/railroadide/railroad/gradle/service/impl/ToolingGradleExecutionService.java +++ b/src/main/java/dev/railroadide/railroad/gradle/service/impl/ToolingGradleExecutionService.java @@ -40,6 +40,23 @@ public ToolingGradleExecutionService(Project project, GradleEnvironment environm this.executor = Objects.requireNonNull(executor); } + private static int findFreePort() { + try (var socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } catch (IOException exception) { + throw new RuntimeException("Failed to find a free port for debugging", exception); + } + } + + private static String bufferToString(Queue buffer) { + var stringBuilder = new StringBuilder(); + for (String line : buffer) { + stringBuilder.append(line).append(System.lineSeparator()); + } + + return stringBuilder.toString(); + } + @Override public GradleTaskExecutionHandle runTask(GradleTaskExecutionRequest request) { var handle = new ToolingGradleTaskExecutionHandle(request); @@ -182,21 +199,4 @@ private void applyDebugConfiguration(GradleTaskExecutionRequest request, debugPort ); } - - private static int findFreePort() { - try (var socket = new ServerSocket(0)) { - return socket.getLocalPort(); - } catch (IOException exception) { - throw new RuntimeException("Failed to find a free port for debugging", exception); - } - } - - private static String bufferToString(Queue buffer) { - var stringBuilder = new StringBuilder(); - for (String line : buffer) { - stringBuilder.append(line).append(System.lineSeparator()); - } - - return stringBuilder.toString(); - } } diff --git a/src/main/java/dev/railroadide/railroad/gradle/service/impl/ToolingGradleModelService.java b/src/main/java/dev/railroadide/railroad/gradle/service/impl/ToolingGradleModelService.java index 1b61bc10..103e0fc7 100644 --- a/src/main/java/dev/railroadide/railroad/gradle/service/impl/ToolingGradleModelService.java +++ b/src/main/java/dev/railroadide/railroad/gradle/service/impl/ToolingGradleModelService.java @@ -1,32 +1,28 @@ package dev.railroadide.railroad.gradle.service.impl; -import com.google.gson.Gson; -import com.google.gson.JsonParseException; -import com.google.gson.reflect.TypeToken; import dev.railroadide.railroad.AppResources; +import dev.railroadide.railroad.Railroad; import dev.railroadide.railroad.gradle.GradleEnvironment; import dev.railroadide.railroad.gradle.model.GradleBuildModel; import dev.railroadide.railroad.gradle.model.GradleModelListener; -import dev.railroadide.railroad.gradle.model.GradleModelMapper; -import dev.railroadide.railroad.gradle.model.GradleProjectModel; -import dev.railroadide.railroad.gradle.model.task.GradleTaskArgType; -import dev.railroadide.railroad.gradle.model.task.GradleTaskArgument; import dev.railroadide.railroad.gradle.service.GradleModelService; import dev.railroadide.railroad.project.Project; import dev.railroadide.railroad.utility.function.ThrowingSupplier; +import dev.railroadide.railroadplugin.dto.FabricDataModel; +import dev.railroadide.railroadplugin.dto.RailroadProject; import org.gradle.tooling.GradleConnector; -import org.gradle.tooling.ModelBuilder; import org.gradle.tooling.ProjectConnection; -import org.gradle.tooling.model.GradleProject; +import org.gradle.tooling.UnknownModelException; import org.gradle.tooling.model.build.BuildEnvironment; import org.gradle.tooling.model.gradle.GradleBuild; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.time.Duration; -import java.util.*; +import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -36,18 +32,15 @@ * the Gradle build model. */ public class ToolingGradleModelService implements GradleModelService { - private static final String TASK_ARGS_PROPERTY = "railroad.taskArgs.output"; private final Project project; private final GradleEnvironment environment; private final Executor executor; private final Object lock = new Object(); private final AtomicReference cachedModel = new AtomicReference<>(); - private volatile CompletableFuture ongoingRefresh = null; - private final Duration modelTimeout = Duration.ofMinutes(3); - private final List listeners = new CopyOnWriteArrayList<>(); + private volatile CompletableFuture ongoingRefresh = null; /** * Creates a new ToolingGradleModelService. @@ -62,20 +55,103 @@ public ToolingGradleModelService(Project project, GradleEnvironment environment, this.executor = Objects.requireNonNull(executor); } + public static GradleBuildModel loadModel(Project project, GradleEnvironment environment) { + GradleConnector connector = GradleConnector.newConnector() + .forProjectDirectory(project.getPath().toFile()); + configureConnector(connector, environment); + + Path initScriptPath = null; + try (ProjectConnection connection = connector.connect()) { + initScriptPath = writeInitScript(); + String[] initScriptArgs = {"--init-script", initScriptPath.toAbsolutePath().toString()}; + connection.newBuild().withArguments(initScriptArgs).run(); + + BuildEnvironment buildEnvironment = connection.model(BuildEnvironment.class) + .withArguments(initScriptArgs) + .get(); + GradleBuild gradleBuild = connection.model(GradleBuild.class) + .withArguments(initScriptArgs) + .get(); + RailroadProject railroadProject = requestOptionalModel(connection, RailroadProject.class, initScriptArgs); + FabricDataModel fabricDataModel = requestOptionalModel(connection, FabricDataModel.class, initScriptArgs); + + String gradleVersion = buildEnvironment.getGradle().getGradleVersion(); + Path rootDir = gradleBuild.getRootProject().getProjectDirectory().toPath(); + + return new GradleBuildModel(gradleVersion, rootDir, fabricDataModel, railroadProject); + } catch (Exception exception) { + throw new RuntimeException("Failed to load Gradle model", exception); + } finally { + if (initScriptPath != null) { + try { + Files.deleteIfExists(initScriptPath); + } catch (Exception ignored) { + } + } + } + } + + private static Path writeInitScript() { + try { + Path path = Files.createTempFile("gradle-init-script", ".gradle"); + path.toFile().deleteOnExit(); + Files.copy(AppResources.getResourceAsStream("scripts/init-gradle-plugin.gradle"), path, StandardCopyOption.REPLACE_EXISTING); + return path; + } catch (Exception exception) { + throw new RuntimeException("Failed to write Gradle init script", exception); + } + } + + private static T requestOptionalModel(ProjectConnection connection, Class modelClass, String[] initScriptArgs) { + try { + return connection.model(modelClass) + .withArguments(initScriptArgs) + .get(); + } catch (UnknownModelException exception) { + Railroad.LOGGER.warn("Gradle model {} is not available; continuing without it", modelClass.getSimpleName()); + return null; + } + } + /** - * Adds a listener to be notified of model loading events. + * Configures the given GradleConnector based on the provided GradleEnvironment. * - * @param listener the listener to add + * @param connector the GradleConnector to configure + * @param environment the GradleEnvironment containing configuration settings */ + public static void configureConnector(GradleConnector connector, GradleEnvironment environment) { + if (environment == null || connector == null) + return; + + if (environment.useWrapper()) { + connector.useBuildDistribution(); + } else { + environment.installationPath().ifPresent(path -> connector.useInstallation(path.toFile())); + environment.userHomePath().ifPresent(path -> connector.useGradleUserHomeDir(path.toFile())); + } + + // TODO: Enable setting Java home via environment.jvm() when custom JVM support is implemented. + // environment.jvm().ifPresent(jvm -> connector.setJavaHome(jvm.javaHome().toFile())); + } + + private static Supplier safely(ThrowingSupplier supplier) { + return () -> { + try { + return supplier.get(); + } catch (RuntimeException exception) { + throw exception; + } catch (Exception exception) { + throw new CompletionException(exception); + } + }; + } + + @Override public void addListener(GradleModelListener listener) { listeners.add(listener); } - /** - * Removes a previously added listener. - * - * @param listener the listener to remove - */ + @Override public void removeListener(GradleModelListener listener) { listeners.remove(listener); } @@ -94,7 +170,9 @@ public CompletableFuture refreshModel(boolean force) { listeners.forEach(GradleModelListener::modelReloadStarted); - ongoingRefresh = CompletableFuture.supplyAsync(safely(this::loadModel), executor) + ongoingRefresh = CompletableFuture.supplyAsync( + safely(() -> ToolingGradleModelService.loadModel(this.project, this.environment)), + executor) .orTimeout(modelTimeout.toMillis(), TimeUnit.MILLISECONDS) .whenComplete((model, throwable) -> { synchronized (lock) { @@ -121,170 +199,4 @@ public CompletableFuture refreshModel(boolean force) { public Optional getCachedModel() { return Optional.ofNullable(cachedModel.get()); } - - private GradleBuildModel loadModel() { - GradleConnector connector = GradleConnector.newConnector() - .forProjectDirectory(project.getPath().toFile()); - configureConnector(connector, environment); - - Path initScriptPath = null; - Path taskArgumentsPath = null; - - try (ProjectConnection connection = connector.connect()) { - BuildEnvironment buildEnvironment = connection.getModel(BuildEnvironment.class); - GradleBuild gradleBuild = connection.getModel(GradleBuild.class); - - String gradleVersion = buildEnvironment.getGradle().getGradleVersion(); - Path rootDir = gradleBuild.getRootProject().getProjectDirectory().toPath(); - - GradleProject rootGradleProject; - Map> taskArguments = Collections.emptyMap(); - - try { - taskArgumentsPath = Files.createTempFile("railroad-task-args", ".json"); - initScriptPath = writeTaskArgumentsInitScript(); - - ModelBuilder projectModelBuilder = connection.model(GradleProject.class) - .withArguments( - "--init-script", initScriptPath.toAbsolutePath().toString(), - ("-D" + TASK_ARGS_PROPERTY + "=" + taskArgumentsPath.toAbsolutePath()) - ); - - rootGradleProject = projectModelBuilder.get(); - taskArguments = readTaskArguments(taskArgumentsPath); - } catch (Exception exception) { - rootGradleProject = connection.getModel(GradleProject.class); - } finally { - deleteIfExists(initScriptPath); - deleteIfExists(taskArgumentsPath); - } - - List projects = new ArrayList<>(); - if (rootGradleProject != null) { - GradleModelMapper.collectProjects(rootGradleProject, projects, taskArguments); - } - - return new GradleBuildModel(gradleVersion, rootDir, projects); - } - } - - /** - * Configures the given GradleConnector based on the provided GradleEnvironment. - * - * @param connector the GradleConnector to configure - * @param environment the GradleEnvironment containing configuration settings - */ - public static void configureConnector(GradleConnector connector, GradleEnvironment environment) { - if (environment.useWrapper()) { - connector.useBuildDistribution(); - } else { - environment.installationPath().ifPresent(path -> connector.useInstallation(path.toFile())); - environment.userHomePath().ifPresent(path -> connector.useGradleUserHomeDir(path.toFile())); - } - - // TODO: Enable setting Java home via environment.jvm() when custom JVM support is implemented. - // environment.jvm().ifPresent(jvm -> connector.setJavaHome(jvm.javaHome().toFile())); - } - - private Path writeTaskArgumentsInitScript() throws IOException { - Path scriptFile = Files.createTempFile("railroad-task-args-init", ".gradle"); - Files.writeString(scriptFile, loadTaskArgsInitScript(), StandardCharsets.UTF_8); - return scriptFile; - } - - private Map> readTaskArguments(Path outputFile) { - if (outputFile == null || !Files.isReadable(outputFile)) - return Collections.emptyMap(); - - try { - String json = Files.readString(outputFile, StandardCharsets.UTF_8); - if (json.isBlank()) - return Collections.emptyMap(); - - var typeToken = new TypeToken>>() { - }.getType(); - Map> parsed = new Gson().fromJson(json, typeToken); - if (parsed == null || parsed.isEmpty()) - return Collections.emptyMap(); - - Map> mapped = new HashMap<>(); - parsed.forEach((taskPath, entries) -> { - if (entries == null) - return; - - List arguments = entries.stream() - .map(this::mapArgument) - .filter(Objects::nonNull) - .toList(); - - mapped.put(taskPath, arguments); - }); - - return mapped; - } catch (IOException | JsonParseException exception) { - return Collections.emptyMap(); - } - } - - private GradleTaskArgument mapArgument(TaskArgumentEntry entry) { - if (entry == null || entry.name == null || entry.type == null) - return null; - - GradleTaskArgType argType = parseArgType(entry.type); - List enumValues = entry.enumValues != null ? entry.enumValues : List.of(); - String defaultValue = entry.defaultValue != null ? entry.defaultValue : ""; - String displayName = entry.displayName != null ? entry.displayName : entry.name; - String description = entry.description != null ? entry.description : ""; - - return new GradleTaskArgument(entry.name, displayName, argType, defaultValue, description, enumValues); - } - - private GradleTaskArgType parseArgType(String type) { - try { - return GradleTaskArgType.valueOf(type); - } catch (IllegalArgumentException exception) { - return GradleTaskArgType.STRING; - } - } - - private void deleteIfExists(Path path) { - if (path == null) - return; - - try { - Files.deleteIfExists(path); - } catch (IOException ignored) { - } - } - - private String loadTaskArgsInitScript() throws IOException { - try (var stream = AppResources.getResourceAsStream("scripts/init-task-args.gradle")) { - if (stream == null) - throw new IOException("Missing init-task-args.gradle resource"); - - String content = new String(stream.readAllBytes(), StandardCharsets.UTF_8); - return content.replace("__TASK_ARGS_PROPERTY__", TASK_ARGS_PROPERTY); - } - } - - private static Supplier safely(ThrowingSupplier supplier) { - return () -> { - try { - return supplier.get(); - } catch (RuntimeException exception) { - throw exception; - } catch (Exception exception) { - throw new CompletionException(exception); - } - }; - } - - private static final class TaskArgumentEntry { - String name; - String displayName; - String type; - String defaultValue; - String description; - List enumValues; - } } diff --git a/src/main/java/dev/railroadide/railroad/gradle/service/impl/ToolingGradleTaskExecutionHandle.java b/src/main/java/dev/railroadide/railroad/gradle/service/impl/ToolingGradleTaskExecutionHandle.java index ffd1fc38..489e3d23 100644 --- a/src/main/java/dev/railroadide/railroad/gradle/service/impl/ToolingGradleTaskExecutionHandle.java +++ b/src/main/java/dev/railroadide/railroad/gradle/service/impl/ToolingGradleTaskExecutionHandle.java @@ -31,16 +31,13 @@ public class ToolingGradleTaskExecutionHandle implements GradleTaskExecutionHand private final List> outputListeners = new CopyOnWriteArrayList<>(); private final List> errorListeners = new CopyOnWriteArrayList<>(); private final List> statusListeners = new CopyOnWriteArrayList<>(); - + private final Queue outputBuffer = new ConcurrentLinkedQueue<>(); + private final Queue errorBuffer = new ConcurrentLinkedQueue<>(); private volatile GradleTaskState state = GradleTaskState.QUEUED; private volatile CompletableFuture resultCompletion = new CompletableFuture<>(); - private volatile CancellationTokenSource cancellationTokenSource; private volatile int debugPort = -1; - private final Queue outputBuffer = new ConcurrentLinkedQueue<>(); - private final Queue errorBuffer = new ConcurrentLinkedQueue<>(); - /** * Creates a new ToolingGradleTaskExecutionHandle for the given request. * diff --git a/src/main/java/dev/railroadide/railroad/gradle/service/task/event/GradleTaskErrorEvent.java b/src/main/java/dev/railroadide/railroad/gradle/service/task/event/GradleTaskErrorEvent.java index fdc0ff4f..a9199867 100644 --- a/src/main/java/dev/railroadide/railroad/gradle/service/task/event/GradleTaskErrorEvent.java +++ b/src/main/java/dev/railroadide/railroad/gradle/service/task/event/GradleTaskErrorEvent.java @@ -5,6 +5,7 @@ import java.util.UUID; // TODO: extend with more error details (e.g., exception type, stack trace, etc.) + /** * Carries information about an error produced by a running Gradle task. * diff --git a/src/main/java/dev/railroadide/railroad/gradle/service/task/event/GradleTaskOutputEvent.java b/src/main/java/dev/railroadide/railroad/gradle/service/task/event/GradleTaskOutputEvent.java index f57658f4..18a26508 100644 --- a/src/main/java/dev/railroadide/railroad/gradle/service/task/event/GradleTaskOutputEvent.java +++ b/src/main/java/dev/railroadide/railroad/gradle/service/task/event/GradleTaskOutputEvent.java @@ -7,9 +7,9 @@ /** * Represents a chunk of Gradle output emitted while a task is running. * - * @param taskId the identifier of the running task - * @param state the current state when the output was produced - * @param output the actual text emitted + * @param taskId the identifier of the running task + * @param state the current state when the output was produced + * @param output the actual text emitted */ public record GradleTaskOutputEvent( UUID taskId, diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/GradleProjectContextMenu.java b/src/main/java/dev/railroadide/railroad/gradle/ui/GradleProjectContextMenu.java new file mode 100644 index 00000000..aa7aad21 --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/GradleProjectContextMenu.java @@ -0,0 +1,65 @@ +package dev.railroadide.railroad.gradle.ui; + +import dev.railroadide.core.ui.RRBorderPane; +import dev.railroadide.core.ui.localized.LocalizedMenuItem; +import dev.railroadide.railroad.ide.projectexplorer.PathItem; +import dev.railroadide.railroad.ide.projectexplorer.ProjectExplorerPane; +import dev.railroadide.railroad.project.Project; +import dev.railroadide.railroad.utility.FileUtils; +import dev.railroadide.railroad.utility.icon.RailroadBrandsIcon; +import dev.railroadide.railroadplugin.dto.RailroadModule; +import javafx.scene.control.ContextMenu; +import javafx.stage.Window; +import org.gradle.tooling.model.GradleProject; +import org.gradle.tooling.model.gradle.GradleScript; +import org.kordamp.ikonli.fontawesome6.FontAwesomeSolid; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.io.File; +import java.nio.file.Path; + +public class GradleProjectContextMenu extends ContextMenu { + public GradleProjectContextMenu(Project project, RailroadModule module) { + super(); + + var openGradleConfig = new LocalizedMenuItem("railroad.gradle.tools.ctx_menu.open_gradle_config", new FontIcon(RailroadBrandsIcon.GRADLE)); + openGradleConfig.setOnAction(event -> { + Path buildFile = findBuildScript(module); + if (buildFile == null) + return; + + // TODO: Eventually we will have a system like Project#getFileManager to handle opening files + Window owner = getOwnerWindow(); + if (owner != null && owner.getScene() != null && owner.getScene().getRoot() instanceof RRBorderPane borderPane) { + ProjectExplorerPane.openFile(project, new PathItem(buildFile), borderPane); + } else { + // Fallback to system handler if we cannot locate the IDE root pane + FileUtils.openInDefaultApplication(buildFile); + } + }); + + var syncItem = new LocalizedMenuItem("railroad.gradle.tools.ctx_menu.sync", new FontIcon(FontAwesomeSolid.SYNC)); + syncItem.setOnAction(event -> project.getGradleManager().getGradleModelService().refreshModel(true)); + + getItems().addAll(openGradleConfig, syncItem); + } + + private Path findBuildScript(RailroadModule module) { + if (module == null || module.getProjectDir() == null) + return null; + + GradleProject gradleProject = module.getGradleProject(); + if (gradleProject == null) + return null; + + GradleScript buildScript = gradleProject.getBuildScript(); + if (buildScript == null) + return null; + + File sourceFile = buildScript.getSourceFile(); + if (sourceFile == null) + return null; + + return sourceFile.toPath(); + } +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/GradleToolsPane.java b/src/main/java/dev/railroadide/railroad/gradle/ui/GradleToolsPane.java new file mode 100644 index 00000000..844a63a2 --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/GradleToolsPane.java @@ -0,0 +1,189 @@ +package dev.railroadide.railroad.gradle.ui; + +import dev.railroadide.core.ui.RRButton; +import dev.railroadide.core.ui.RRHBox; +import dev.railroadide.core.ui.RRToggleButton; +import dev.railroadide.core.ui.RRVBox; +import dev.railroadide.core.ui.localized.LocalizedTab; +import dev.railroadide.core.ui.localized.LocalizedTooltip; +import dev.railroadide.core.ui.styling.ButtonSize; +import dev.railroadide.core.ui.styling.ButtonVariant; +import dev.railroadide.railroad.Railroad; +import dev.railroadide.railroad.gradle.GradleSettings; +import dev.railroadide.railroad.gradle.model.GradleBuildModel; +import dev.railroadide.railroad.gradle.model.GradleModelListener; +import dev.railroadide.railroad.gradle.project.GradleManager; +import dev.railroadide.railroad.gradle.ui.deps.GradleDependenciesPane; +import dev.railroadide.railroad.gradle.ui.task.GradleTasksPane; +import dev.railroadide.railroad.project.Project; +import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.control.ButtonBase; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import lombok.Getter; +import org.kordamp.ikonli.Ikon; +import org.kordamp.ikonli.fontawesome6.FontAwesomeSolid; +import org.kordamp.ikonli.javafx.StackedFontIcon; + +@Getter +public class GradleToolsPane extends RRVBox { + private final TabPane tabPane; + private final Tab tasksTab; + private final Tab dependenciesTab; + + public GradleToolsPane(Project project) { + super(); + getStyleClass().add("gradle-tools-pane"); + + GradleManager gradleManager = project.getGradleManager(); + var modelService = gradleManager.getGradleModelService(); + + var syncButton = createButtonBarButton( + FontAwesomeSolid.SYNC, + "railroad.gradle.tools.button.sync.tooltip", + "sync-button", + false + ); + syncButton.setOnAction(event -> + gradleManager.getGradleModelService().refreshModel(true)); + + var downloadSourcesButton = createButtonBarButton( + FontAwesomeSolid.DOWNLOAD, + "railroad.gradle.tools.button.downloadsources.tooltip", + "download-sources-button", + false + ); + downloadSourcesButton.setOnAction(event -> { + Railroad.LOGGER.info("Downloading Gradle sources..."); + downloadSourcesButton.setDisable(true); + gradleManager.downloadAllSources().whenComplete((ignored, throwable) -> { + if (throwable != null) { + Railroad.LOGGER.error("Failed to download Gradle sources", throwable); + } else { + Railroad.LOGGER.info("Gradle sources downloaded successfully"); + } + + Platform.runLater(() -> downloadSourcesButton.setDisable(false)); + }); + }); + + var offlineIcon = new StackedFontIcon(); + offlineIcon.setIconCodes(FontAwesomeSolid.WIFI, FontAwesomeSolid.SLASH); + var toggleOfflineButton = createButtonBarButton( + offlineIcon, + "railroad.gradle.tools.button.toggleoffline.tooltip", + "toggle-offline-button", + true + ); + toggleOfflineButton.setOnAction(event -> { + GradleSettings gradleSettings = gradleManager.getGradleSettings(); + boolean newOfflineMode = !gradleSettings.isOfflineMode(); + gradleSettings.setOfflineMode(newOfflineMode); + gradleManager.saveSettings(); + ((RRToggleButton) toggleOfflineButton).setSelected(newOfflineMode); + }); + + var listener = new GradleModelListener() { + private void setButtonsDisabled(boolean disabled) { + Platform.runLater(() -> { + syncButton.setDisable(disabled); + downloadSourcesButton.setDisable(disabled); + toggleOfflineButton.setDisable(disabled); + }); + } + + @Override + public void modelReloadStarted() { + setButtonsDisabled(true); + } + + @Override + public void modelReloadSucceeded(GradleBuildModel model) { + setButtonsDisabled(false); + } + + @Override + public void modelReloadFailed(Throwable error) { + setButtonsDisabled(false); + } + }; + modelService.addListener(listener); + + var buttonBar = new RRHBox(2, syncButton, downloadSourcesButton, toggleOfflineButton); + buttonBar.getStyleClass().add("gradle-tools-buttonbar"); + + getChildren().add(buttonBar); + + this.tasksTab = new LocalizedTab("railroad.gradle.tools.tasks", new GradleTasksPane(project)); + this.dependenciesTab = new LocalizedTab("railroad.gradle.tools.dependencies", new GradleDependenciesPane(project)); + + this.tabPane = new TabPane(tasksTab, dependenciesTab); + tabPane.getStyleClass().add("gradle-tools-tabpane"); + tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + getChildren().add(tabPane); + VBox.setVgrow(tabPane, Priority.ALWAYS); + } + + private static ButtonBase createButtonBarButton(Ikon ikon, String tooltipKey, String styleClass, boolean toggle) { + return toggle + ? createToggleButtonBarButton(ikon, tooltipKey, styleClass) + : createButtonBarButton(ikon, tooltipKey, styleClass); + } + + private static ButtonBase createButtonBarButton(Node ikon, String tooltipKey, String styleClass, boolean toggle) { + return toggle + ? createToggleButtonBarButton(ikon, tooltipKey, styleClass) + : createButtonBarButton(ikon, tooltipKey, styleClass); + } + + private static RRToggleButton createToggleButtonBarButton(Node graphic, String tooltipKey, String styleClass) { + var button = new RRToggleButton("", graphic); + button.setSquare(true); + button.setButtonSize(ButtonSize.SMALL); + button.setVariant(ButtonVariant.GHOST); + button.setTooltip(new LocalizedTooltip(tooltipKey)); + button.getStyleClass().addAll("gradle-tools-buttonbar-button", styleClass); + return button; + } + + private static RRToggleButton createToggleButtonBarButton(Ikon graphic, String tooltipKey, String styleClass) { + var button = new RRToggleButton("", graphic); + button.setSquare(true); + button.setButtonSize(ButtonSize.SMALL); + button.setVariant(ButtonVariant.GHOST); + button.setTooltip(new LocalizedTooltip(tooltipKey)); + button.getStyleClass().addAll("gradle-tools-buttonbar-button", styleClass); + return button; + } + + private static RRButton createButtonBarButton(Node graphic, String tooltipKey, String styleClass) { + var button = new RRButton("", graphic); + button.setSquare(true); + button.setButtonSize(ButtonSize.SMALL); + button.setVariant(ButtonVariant.GHOST); + button.setTooltip(new LocalizedTooltip(tooltipKey)); + button.getStyleClass().addAll("gradle-tools-buttonbar-button", styleClass); + return button; + } + + private static RRButton createButtonBarButton(Ikon graphic, String tooltipKey, String styleClass) { + var button = new RRButton("", graphic); + button.setSquare(true); + button.setButtonSize(ButtonSize.SMALL); + button.setVariant(ButtonVariant.GHOST); + button.setTooltip(new LocalizedTooltip(tooltipKey)); + button.getStyleClass().addAll("gradle-tools-buttonbar-button", styleClass); + return button; + } + + public boolean isTasksTabSelected() { + return this.tabPane.getSelectionModel().getSelectedItem() == tasksTab; + } + + public boolean isDependenciesTabSelected() { + return this.tabPane.getSelectionModel().getSelectedItem() == dependenciesTab; + } +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/GradleTreeBuilder.java b/src/main/java/dev/railroadide/railroad/gradle/ui/GradleTreeBuilder.java new file mode 100644 index 00000000..7ee4d45d --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/GradleTreeBuilder.java @@ -0,0 +1,29 @@ +package dev.railroadide.railroad.gradle.ui; + +import dev.railroadide.railroad.gradle.ui.tree.GradleTreeElement; +import dev.railroadide.railroad.project.Project; +import javafx.collections.ObservableList; +import javafx.scene.control.TreeItem; + +public interface GradleTreeBuilder { + TreeItem buildTree(Project project, ObservableList elements); + + default String getParentProjectPath(String projectPath) { + if (projectPath == null || ":".equals(projectPath)) + return null; + + String trimmed = projectPath.startsWith(":") ? projectPath.substring(1) : projectPath; + if (trimmed.isEmpty()) + return null; + + int lastSeparator = trimmed.lastIndexOf(':'); + if (lastSeparator < 0) + return ":"; + + String parentSegments = trimmed.substring(0, lastSeparator); + if (parentSegments.isEmpty()) + return ":"; + + return ":" + parentSegments; + } +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/GradleTreeViewPane.java b/src/main/java/dev/railroadide/railroad/gradle/ui/GradleTreeViewPane.java new file mode 100644 index 00000000..f124e59c --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/GradleTreeViewPane.java @@ -0,0 +1,97 @@ +package dev.railroadide.railroad.gradle.ui; + +import dev.railroadide.core.ui.RRVBox; +import dev.railroadide.railroad.Railroad; +import dev.railroadide.railroad.gradle.model.GradleBuildModel; +import dev.railroadide.railroad.gradle.model.GradleModelListener; +import dev.railroadide.railroad.gradle.service.GradleModelService; +import dev.railroadide.railroad.gradle.ui.tree.GradleTreeCell; +import dev.railroadide.railroad.gradle.ui.tree.GradleTreeElement; +import dev.railroadide.railroad.project.Project; +import io.github.palexdev.materialfx.controls.MFXProgressSpinner; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.TreeView; +import javafx.scene.layout.StackPane; + +import java.util.Collection; +import java.util.concurrent.atomic.AtomicBoolean; + +public abstract class GradleTreeViewPane extends RRVBox { + private final ObservableList elements = FXCollections.observableArrayList(); + + private final TreeView treeView = new TreeView<>(); + private final MFXProgressSpinner loadingSpinner = new MFXProgressSpinner(); + private final StackPane loadingContainer = new StackPane(loadingSpinner); + private final AtomicBoolean isLoading = new AtomicBoolean(true); + + public GradleTreeViewPane(Project project) { + super(); + getStyleClass().add("gradle-tool-content-pane"); + + loadingSpinner.getStyleClass().add("gradle-tool-loading-spinner"); + loadingContainer.setAlignment(Pos.CENTER); + loadingContainer.prefHeightProperty().bind(heightProperty()); + loadingContainer.prefWidthProperty().bind(widthProperty()); + + treeView.getStyleClass().add("gradle-tasks-tree-view"); + treeView.setShowRoot(false); + treeView.setCellFactory(param -> new GradleTreeCell()); + treeView.prefHeightProperty().bind(heightProperty()); + elements.addListener((ListChangeListener) change -> Platform.runLater(() -> { + GradleTreeBuilder treeBuilder = createTreeBuilder(); + treeView.setRoot(treeBuilder.buildTree(project, elements)); + })); + + updateLoadingState(); + + GradleModelService modelService = project.getGradleManager().getGradleModelService(); + modelService.refreshModel(true); + modelService.addListener(new GradleModelListener() { + @Override + public void modelReloadStarted() { + isLoading.set(true); + updateLoadingState(); + } + + @Override + public void modelReloadSucceeded(GradleBuildModel model) { + isLoading.set(false); + elements.setAll(getElementsFromModel(modelService, model)); + updateLoadingState(); + } + + @Override + public void modelReloadFailed(Throwable error) { + Railroad.LOGGER.error("Failed to reload Gradle model", error); + isLoading.set(false); + updateLoadingState(); + } + }); + } + + protected abstract GradleTreeBuilder createTreeBuilder(); + + protected abstract Collection getElementsFromModel(GradleModelService modelService, GradleBuildModel model); + + protected void updateLoadingState() { + Platform.runLater(() -> { + ObservableList children = getChildren(); + if (isLoading.get()) { + if (!children.contains(loadingContainer)) { + children.clear(); + children.add(loadingContainer); + } + } else { + if (!children.contains(treeView)) { + children.clear(); + children.add(treeView); + } + } + }); + } +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/deps/GradleDependenciesPane.java b/src/main/java/dev/railroadide/railroad/gradle/ui/deps/GradleDependenciesPane.java new file mode 100644 index 00000000..375302de --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/deps/GradleDependenciesPane.java @@ -0,0 +1,35 @@ +package dev.railroadide.railroad.gradle.ui.deps; + +import dev.railroadide.railroad.gradle.model.GradleBuildModel; +import dev.railroadide.railroad.gradle.service.GradleModelService; +import dev.railroadide.railroad.gradle.ui.GradleTreeBuilder; +import dev.railroadide.railroad.gradle.ui.GradleTreeViewPane; +import dev.railroadide.railroad.project.Project; +import dev.railroadide.railroadplugin.dto.RailroadConfiguration; +import dev.railroadide.railroadplugin.dto.RailroadProject; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + +public class GradleDependenciesPane extends GradleTreeViewPane { + public GradleDependenciesPane(Project project) { + super(project); + } + + @Override + protected GradleTreeBuilder createTreeBuilder() { + return new GradleDependencyTreeBuilder(); + } + + @Override + protected Collection getElementsFromModel(GradleModelService modelService, GradleBuildModel model) { + return List.copyOf(modelService.getCachedModel() + .map(GradleBuildModel::project) + .map(RailroadProject::getModules) + .map(Collection::stream) + .orElseGet(Stream::empty) + .flatMap(module -> module.getConfigurations().stream()) + .toList()); + } +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/deps/GradleDependencyTreeBuilder.java b/src/main/java/dev/railroadide/railroad/gradle/ui/deps/GradleDependencyTreeBuilder.java new file mode 100644 index 00000000..6e43abfc --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/deps/GradleDependencyTreeBuilder.java @@ -0,0 +1,112 @@ +package dev.railroadide.railroad.gradle.ui.deps; + +import dev.railroadide.railroad.gradle.ui.GradleTreeBuilder; +import dev.railroadide.railroad.gradle.ui.tree.GradleConfigurationElement; +import dev.railroadide.railroad.gradle.ui.tree.GradleDependencyElement; +import dev.railroadide.railroad.gradle.ui.tree.GradleProjectElement; +import dev.railroadide.railroad.gradle.ui.tree.GradleTreeElement; +import dev.railroadide.railroad.project.Project; +import dev.railroadide.railroadplugin.dto.RailroadConfiguration; +import dev.railroadide.railroadplugin.dto.RailroadDependency; +import dev.railroadide.railroadplugin.dto.RailroadModule; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.TreeItem; +import org.gradle.tooling.model.DomainObjectSet; + +import java.util.*; + +public class GradleDependencyTreeBuilder implements GradleTreeBuilder { + @Override + public TreeItem buildTree(Project project, ObservableList elements) { + TreeItem root = new TreeItem<>(); + + List rootConfigs = elements.stream() + .filter(Objects::nonNull) + .filter(cfg -> cfg.getParent() == null) + .filter(cfg -> cfg.getDependencies() != null && !cfg.getDependencies().isEmpty()) + .toList(); + + Map> configsByModule = new HashMap<>(); + for (RailroadConfiguration cfg : elements) { + if (cfg == null) + continue; + + RailroadModule module = cfg.getParent(); + if (cfg.getDependencies() == null || cfg.getDependencies().isEmpty()) + continue; + + configsByModule.computeIfAbsent(module, k -> new ArrayList<>()).add(cfg); + } + + for (Map.Entry> entry : configsByModule.entrySet()) { + RailroadModule module = entry.getKey(); + List configs = entry.getValue(); + + TreeItem moduleNode = module != null + ? new TreeItem<>(new GradleProjectElement(project, module)) + : root; + + if (module != null) { + root.getChildren().add(moduleNode); + } + + for (RailroadConfiguration configurationTree : configs) { + DomainObjectSet dependencies = configurationTree.getDependencies(); + if (dependencies == null || dependencies.isEmpty()) + continue; + + TreeItem configurationNode = new TreeItem<>( + new GradleConfigurationElement(configurationTree.getName())); + moduleNode.getChildren().add(configurationNode); + + addDependencies(configurationNode, dependencies); + } + } + + for (RailroadConfiguration configurationTree : rootConfigs) { + DomainObjectSet dependencies = configurationTree.getDependencies(); + if (dependencies == null || dependencies.isEmpty()) + continue; + + TreeItem configurationNode = new TreeItem<>( + new GradleConfigurationElement(configurationTree.getName())); + root.getChildren().add(configurationNode); + + addDependencies(configurationNode, dependencies); + } + + sortTree(root); + return root; + } + + private void addDependencies(TreeItem parent, Collection dependencies) { + if (dependencies == null) + return; + + for (RailroadDependency dependency : dependencies) { + if (dependency == null) + continue; + + TreeItem dependencyNode = new TreeItem<>(new GradleDependencyElement(dependency)); + parent.getChildren().add(dependencyNode); + + addDependencies(dependencyNode, dependency.getChildren()); + } + } + + private void sortTree(TreeItem node) { + Comparator> comparator = Comparator.comparing( + item -> { + GradleTreeElement element = item.getValue(); + return element == null ? "" : element.getName(); + }, + String.CASE_INSENSITIVE_ORDER + ); + + FXCollections.sort(node.getChildren(), comparator); + for (TreeItem child : node.getChildren()) { + sortTree(child); + } + } +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/task/GradleTaskContextMenu.java b/src/main/java/dev/railroadide/railroad/gradle/ui/task/GradleTaskContextMenu.java new file mode 100644 index 00000000..7497b69d --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/task/GradleTaskContextMenu.java @@ -0,0 +1,106 @@ +package dev.railroadide.railroad.gradle.ui.task; + +import dev.railroadide.core.ui.localized.LocalizedMenuItem; +import dev.railroadide.railroad.ide.runconfig.RunConfiguration; +import dev.railroadide.railroad.ide.runconfig.RunConfigurationManager; +import dev.railroadide.railroad.ide.runconfig.RunConfigurationTypes; +import dev.railroadide.railroad.ide.runconfig.defaults.data.GradleRunConfigurationData; +import dev.railroadide.railroad.java.JDKManager; +import dev.railroadide.railroad.project.Project; +import dev.railroadide.railroadplugin.dto.RailroadGradleTask; +import dev.railroadide.railroadplugin.dto.RailroadModule; +import javafx.scene.control.ContextMenu; +import org.jetbrains.annotations.NotNull; +import org.kordamp.ikonli.fontawesome6.FontAwesomeSolid; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.Optional; + +public class GradleTaskContextMenu extends ContextMenu { + public GradleTaskContextMenu(Project project, RailroadGradleTask task) { + super(); + + var runIcon = new FontIcon(FontAwesomeSolid.PLAY); + runIcon.getStyleClass().add("run-button"); + + var runItem = new LocalizedMenuItem("railroad.runconfig.run.tooltip", runIcon); + runItem.setOnAction(event -> { + var runConfiguration = getOrCreateRunConfig(project, task); + runConfiguration.run(project); + }); + + var debugIcon = new FontIcon(FontAwesomeSolid.BUG); + debugIcon.getStyleClass().add("debug-button"); + + var debugItem = new LocalizedMenuItem("railroad.runconfig.debug.tooltip", debugIcon); + debugItem.setOnAction(event -> { + var runConfiguration = getOrCreateRunConfig(project, task); + + runConfiguration.debug(project); + }); + + getItems().addAll(runItem, debugItem); + } + + /** + * Get an existing run configuration for the given Gradle task, or create a new one if it doesn't exist. + * + * @param project the current project + * @param task the Gradle task + * @return the run configuration + */ + public static @NotNull RunConfiguration getOrCreateRunConfig(Project project, RailroadGradleTask task) { + RunConfigurationManager runConfigManager = project.getRunConfigManager(); + @SuppressWarnings("unchecked") + Optional> existingRunConfig = runConfigManager.getConfigurations().stream() + .filter(configuration -> hasExistingRunConfig(task, configuration)) + .map(configuration -> (RunConfiguration) configuration) + .findFirst(); + + return existingRunConfig.orElseGet(() -> createRunConfig(task, runConfigManager)); + } + + /** + * Create a new run configuration for the given Gradle task. + * + * @param task the Gradle task + * @param runConfigManager the run configuration manager + * @return the newly created run configuration + */ + public static @NotNull RunConfiguration createRunConfig(RailroadGradleTask task, RunConfigurationManager runConfigManager) { + var configurationData = new GradleRunConfigurationData(); + RailroadModule module = task.module(); + if (module == null || module.getGradleProject() == null) + throw new IllegalStateException("Cannot create run configuration: module or Gradle project is null"); + + configurationData.setGradleProjectPath(module.getGradleProject().getProjectDirectory().toPath()); + configurationData.setTask(task.getName()); + configurationData.setJavaHome(JDKManager.getDefaultJDK()); + configurationData.setName(module.getName() + " [" + task.getName() + "]"); + + var runConfiguration = new RunConfiguration<>(RunConfigurationTypes.GRADLE, configurationData); + runConfigManager.addConfiguration(runConfiguration); + runConfigManager.setSelectedConfiguration(runConfiguration); + return runConfiguration; + } + + /** + * Check if the given run configuration corresponds to the given Gradle task. + * + * @param task the Gradle task + * @param configuration the run configuration + * @return true if the run configuration corresponds to the Gradle task, false otherwise + */ + public static boolean hasExistingRunConfig(RailroadGradleTask task, RunConfiguration configuration) { + if (configuration.type() != RunConfigurationTypes.GRADLE) + return false; + + var data = (GradleRunConfigurationData) configuration.data(); + RailroadModule module = task.module(); + if (module == null || module.getGradleProject() == null) + return false; + + return data.getGradleProjectPath().equals(module.getGradleProject().getProjectDirectory().toPath()) + && data.getTask().equals(task.getName()); + } +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/task/GradleTaskTreeBuilder.java b/src/main/java/dev/railroadide/railroad/gradle/ui/task/GradleTaskTreeBuilder.java new file mode 100644 index 00000000..bcd185fd --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/task/GradleTaskTreeBuilder.java @@ -0,0 +1,131 @@ +package dev.railroadide.railroad.gradle.ui.task; + +import dev.railroadide.railroad.gradle.ui.GradleTreeBuilder; +import dev.railroadide.railroad.gradle.ui.tree.GradleProjectElement; +import dev.railroadide.railroad.gradle.ui.tree.GradleTaskElement; +import dev.railroadide.railroad.gradle.ui.tree.GradleTaskGroupElement; +import dev.railroadide.railroad.gradle.ui.tree.GradleTreeElement; +import dev.railroadide.railroad.project.Project; +import dev.railroadide.railroad.utility.StringUtils; +import dev.railroadide.railroadplugin.dto.RailroadGradleTask; +import dev.railroadide.railroadplugin.dto.RailroadModule; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.TreeItem; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class GradleTaskTreeBuilder implements GradleTreeBuilder { + @Override + public TreeItem buildTree(Project project, ObservableList elements) { + TreeItem root = new TreeItem<>(); + + Map> tasksByProject = elements.stream() + .collect(Collectors.groupingBy(RailroadGradleTask::module)); + + Map projectsByPath = tasksByProject.keySet().stream() + .collect(Collectors.toMap(RailroadModule::getPath, Function.identity())); + + Map> projectNodes = new ConcurrentHashMap<>(); + + for (RailroadModule module : tasksByProject.keySet()) { + ensureProjectNode(project, module, projectsByPath, projectNodes, root); + } + + for (Map.Entry> entry : tasksByProject.entrySet()) { + TreeItem projectNode = projectNodes.get(entry.getKey().getPath()); + if (projectNode == null) + continue; + + addTasksToProjectNode(project, projectNode, entry.getValue()); + } + + sortTree(root); + return root; + } + + private TreeItem ensureProjectNode( + Project project, + RailroadModule module, + Map projectsByPath, + Map> projectNodes, + TreeItem root + ) { + return projectNodes.computeIfAbsent(module.getPath(), path -> { + TreeItem parentNode = root; + String parentPath = getParentProjectPath(path); + if (parentPath != null) { + RailroadModule parentProject = projectsByPath.get(parentPath); + if (parentProject != null) { + parentNode = ensureProjectNode(project, parentProject, projectsByPath, projectNodes, root); + } + } + + TreeItem node = + new TreeItem<>(new GradleProjectElement(project, module)); + parentNode.getChildren().add(node); + return node; + }); + } + + private void addTasksToProjectNode(Project project, TreeItem projectNode, + List projectTasks) { + Map> tasksByGroup = projectTasks.stream() + .collect(Collectors.groupingBy(task -> { + String group = task.getGroup(); + return group == null ? "" : StringUtils.capitalizeFirstLetterOfEachWord(group); + }, HashMap::new, Collectors.toList())); + + for (Map.Entry> groupEntry : tasksByGroup.entrySet()) { + String groupName = groupEntry.getKey(); + List groupTasks = groupEntry.getValue(); + + TreeItem groupNode = new TreeItem<>( + new GradleTaskGroupElement(groupName)); + projectNode.getChildren().add(groupNode); + + for (RailroadGradleTask task : groupTasks) { + TreeItem taskNode = + new TreeItem<>(new GradleTaskElement(project, task)); + groupNode.getChildren().add(taskNode); + } + } + } + + private void sortTree(TreeItem node) { + Comparator> comparator = + Comparator., Integer>comparing( + item -> typeRank(item.getValue()) + ).thenComparing( + item -> { + GradleTreeElement element = item.getValue(); + return element == null ? "" : element.getName(); + }, + String.CASE_INSENSITIVE_ORDER + ); + + FXCollections.sort(node.getChildren(), comparator); + for (TreeItem child : node.getChildren()) { + sortTree(child); + } + } + + private int typeRank(GradleTreeElement element) { + if (element instanceof GradleTaskElement) + return 0; + + if (element instanceof GradleTaskGroupElement) + return 1; + + if (element instanceof GradleProjectElement) + return 2; + + return Integer.MAX_VALUE; + } +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/task/GradleTasksPane.java b/src/main/java/dev/railroadide/railroad/gradle/ui/task/GradleTasksPane.java new file mode 100644 index 00000000..3d835756 --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/task/GradleTasksPane.java @@ -0,0 +1,41 @@ +package dev.railroadide.railroad.gradle.ui.task; + +import dev.railroadide.railroad.gradle.model.GradleBuildModel; +import dev.railroadide.railroad.gradle.service.GradleModelService; +import dev.railroadide.railroad.gradle.ui.GradleTreeBuilder; +import dev.railroadide.railroad.gradle.ui.GradleTreeViewPane; +import dev.railroadide.railroad.project.Project; +import dev.railroadide.railroadplugin.dto.RailroadGradleTask; +import dev.railroadide.railroadplugin.dto.RailroadModule; +import dev.railroadide.railroadplugin.dto.RailroadProject; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public class GradleTasksPane extends GradleTreeViewPane { + public GradleTasksPane(Project project) { + super(project); + } + + @Override + protected GradleTreeBuilder createTreeBuilder() { + return new GradleTaskTreeBuilder(); + } + + @Override + protected Collection getElementsFromModel(GradleModelService modelService, GradleBuildModel model) { + Optional cachedModel = modelService.getCachedModel(); + if (cachedModel.isEmpty()) + return List.of(); + + RailroadProject railroadProject = cachedModel.get().project(); + List tasks = new ArrayList<>(); + for (RailroadModule module : railroadProject.getModules()) { + tasks.addAll(module.getTasks()); + } + + return tasks; + } +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleConfigurationElement.java b/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleConfigurationElement.java new file mode 100644 index 00000000..2f0c1416 --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleConfigurationElement.java @@ -0,0 +1,20 @@ +package dev.railroadide.railroad.gradle.ui.tree; + +import org.kordamp.ikonli.Ikon; +import org.kordamp.ikonli.fontawesome6.FontAwesomeSolid; + +public class GradleConfigurationElement extends GradleTreeElement { + public GradleConfigurationElement(String name) { + super(name); + } + + @Override + public Ikon getIcon() { + return FontAwesomeSolid.FOLDER; + } + + @Override + public String getStyleClass() { + return "gradle-configuration-element"; + } +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleDependencyElement.java b/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleDependencyElement.java new file mode 100644 index 00000000..f8e1a9e8 --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleDependencyElement.java @@ -0,0 +1,28 @@ +package dev.railroadide.railroad.gradle.ui.tree; + +import dev.railroadide.railroadplugin.dto.RailroadDependency; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.kordamp.ikonli.Ikon; +import org.kordamp.ikonli.fontawesome6.FontAwesomeSolid; + +@Getter +public class GradleDependencyElement extends GradleTreeElement { + private final RailroadDependency dependencyNode; + + public GradleDependencyElement(@NotNull RailroadDependency dependencyNode) { + super(dependencyNode.getGroup() + ":" + dependencyNode.getName() + ":" + dependencyNode.getVersion()); + this.dependencyNode = dependencyNode; + } + + @Override + public Ikon getIcon() { + return FontAwesomeSolid.BOOK; + } + + @Override + public String getStyleClass() { + return "gradle-dependency-element"; + } + +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleProjectElement.java b/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleProjectElement.java new file mode 100644 index 00000000..07cb3a41 --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleProjectElement.java @@ -0,0 +1,39 @@ +package dev.railroadide.railroad.gradle.ui.tree; + +import dev.railroadide.railroad.gradle.ui.GradleProjectContextMenu; +import dev.railroadide.railroad.project.Project; +import dev.railroadide.railroad.utility.icon.RailroadBrandsIcon; +import dev.railroadide.railroadplugin.dto.RailroadModule; +import javafx.scene.control.ContextMenu; +import org.kordamp.ikonli.Ikon; + +import java.util.Objects; + +public class GradleProjectElement extends GradleTreeElement { + private final Project project; + private final RailroadModule module; + + public GradleProjectElement(Project project, RailroadModule module) { + super(Objects.requireNonNull(module, "module must not be null").getName() == null + ? "Unnamed Project" + : Objects.requireNonNull(module, "module must not be null").getName()); + + this.project = project; + this.module = Objects.requireNonNull(module, "module must not be null"); + } + + @Override + public Ikon getIcon() { + return RailroadBrandsIcon.GRADLE; + } + + @Override + public String getStyleClass() { + return "gradle-project-element"; + } + + @Override + public ContextMenu getContextMenu() { + return new GradleProjectContextMenu(this.project, this.module); + } +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleTaskElement.java b/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleTaskElement.java new file mode 100644 index 00000000..a0096740 --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleTaskElement.java @@ -0,0 +1,51 @@ +package dev.railroadide.railroad.gradle.ui.tree; + +import dev.railroadide.railroad.gradle.ui.task.GradleTaskContextMenu; +import dev.railroadide.railroad.project.Project; +import dev.railroadide.railroadplugin.dto.RailroadGradleTask; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Tooltip; +import lombok.Getter; +import org.kordamp.ikonli.Ikon; +import org.kordamp.ikonli.devicons.Devicons; + +@Getter +public class GradleTaskElement extends GradleTreeElement { + private final Project project; + private final RailroadGradleTask task; + + public GradleTaskElement(Project project, RailroadGradleTask task) { + super(task != null ? task.getName() : "Unknown Task"); + if (project == null) + throw new IllegalArgumentException("Project cannot be null"); + if (task == null) + throw new IllegalArgumentException("Task cannot be null"); + + this.project = project; + this.task = task; + } + + @Override + public Ikon getIcon() { + return Devicons.TERMINAL; + } + + @Override + public String getStyleClass() { + return "gradle-task-element"; + } + + @Override + public Tooltip getTooltip() { + String description = this.task.getDescription(); + if (description == null || description.isEmpty()) + return null; + + return new Tooltip(description); + } + + @Override + public ContextMenu getContextMenu() { + return new GradleTaskContextMenu(this.project, this.task); + } +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleTaskGroupElement.java b/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleTaskGroupElement.java new file mode 100644 index 00000000..4cfc5075 --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleTaskGroupElement.java @@ -0,0 +1,20 @@ +package dev.railroadide.railroad.gradle.ui.tree; + +import org.kordamp.ikonli.Ikon; +import org.kordamp.ikonli.fontawesome6.FontAwesomeSolid; + +public class GradleTaskGroupElement extends GradleTreeElement { + public GradleTaskGroupElement(String name) { + super("".equals(name) ? "Other" : name); + } + + @Override + public Ikon getIcon() { + return FontAwesomeSolid.FOLDER; + } + + @Override + public String getStyleClass() { + return "gradle-tasks-group-element"; + } +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleTreeCell.java b/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleTreeCell.java new file mode 100644 index 00000000..471ae39f --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleTreeCell.java @@ -0,0 +1,46 @@ +package dev.railroadide.railroad.gradle.ui.tree; + +import dev.railroadide.railroad.gradle.ui.task.GradleTaskContextMenu; +import javafx.scene.control.TreeCell; +import org.kordamp.ikonli.javafx.FontIcon; + +public class GradleTreeCell extends TreeCell { + private final FontIcon icon = new FontIcon(); + + public GradleTreeCell() { + super(); + icon.setIconSize(16); + } + + @Override + protected void updateItem(GradleTreeElement item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + } else { + setText(item.getName()); + icon.getStyleClass().removeIf(styleClass -> + styleClass.equals("gradle-project-element") || + styleClass.equals("gradle-tasks-group-element") || + styleClass.equals("gradle-task-element") + ); + icon.getStyleClass().add(item.getStyleClass()); + icon.setIconCode(item.getIcon()); + setGraphic(icon); + setTooltip(item.getTooltip()); + setContextMenu(item.getContextMenu()); + setOnMouseClicked(event -> { + if (event.getClickCount() == 2 && !isEmpty()) { + if (item instanceof GradleTaskElement taskElement) { + var runConfiguration = GradleTaskContextMenu.getOrCreateRunConfig( + taskElement.getProject(), + taskElement.getTask() + ); + runConfiguration.run(taskElement.getProject()); + } + } + }); + } + } +} diff --git a/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleTreeElement.java b/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleTreeElement.java new file mode 100644 index 00000000..ae4771a7 --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/gradle/ui/tree/GradleTreeElement.java @@ -0,0 +1,32 @@ +package dev.railroadide.railroad.gradle.ui.tree; + +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Tooltip; +import lombok.Getter; +import org.kordamp.ikonli.Ikon; + +@Getter +public abstract class GradleTreeElement { + private final String name; + + public GradleTreeElement(String name) { + this.name = name; + } + + public abstract Ikon getIcon(); + + public abstract String getStyleClass(); + + public Tooltip getTooltip() { + return null; + } + + public ContextMenu getContextMenu() { + return null; + } + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/dev/railroadide/railroad/ide/IDESetup.java b/src/main/java/dev/railroadide/railroad/ide/IDESetup.java index 08353fa1..a5f73546 100644 --- a/src/main/java/dev/railroadide/railroad/ide/IDESetup.java +++ b/src/main/java/dev/railroadide/railroad/ide/IDESetup.java @@ -8,6 +8,7 @@ import dev.railroadide.core.ui.RRVBox; import dev.railroadide.railroad.Railroad; import dev.railroadide.railroad.Services; +import dev.railroadide.railroad.gradle.ui.GradleToolsPane; import dev.railroadide.railroad.ide.projectexplorer.ProjectExplorerPane; import dev.railroadide.railroad.ide.runconfig.RunConfiguration; import dev.railroadide.railroad.ide.runconfig.ui.RunConfigurationEditorPane; @@ -15,9 +16,16 @@ import dev.railroadide.railroad.ide.ui.IDEWelcomePane; import dev.railroadide.railroad.ide.ui.ImageViewerPane; import dev.railroadide.railroad.ide.ui.StatusBarPane; -import dev.railroadide.railroad.ide.ui.setup.*; +import dev.railroadide.railroad.ide.ui.setup.IDEMenuBarFactory; +import dev.railroadide.railroad.ide.ui.setup.PaneIconBarFactory; +import dev.railroadide.railroad.ide.ui.setup.RunControlsPane; +import dev.railroadide.railroad.ide.ui.setup.TerminalFactory; +import dev.railroadide.railroad.project.FacetDetectedEvent; import dev.railroadide.railroad.project.Project; +import dev.railroadide.railroad.project.facet.Facet; +import dev.railroadide.railroad.project.facet.FacetManager; import dev.railroadide.railroad.settings.keybinds.KeybindHandler; +import dev.railroadide.railroad.utility.icon.RailroadBrandsIcon; import dev.railroadide.railroad.window.WindowBuilder; import dev.railroadide.railroadpluginapi.events.ProjectEvent; import javafx.application.Platform; @@ -60,7 +68,6 @@ public static Scene createIDEScene(Project project) { leftPane.addTab("Project", new ProjectExplorerPane(project, root)); var rightPane = new DetachableTabPane(); - rightPane.addTab("Properties", NotImplementedPaneFactory.create()); var editorPane = new DetachableTabPane(); editorPane.addTab("Welcome", new IDEWelcomePane()); @@ -78,6 +85,17 @@ public static Scene createIDEScene(Project project) { mainSplit.setDividerPositions(0.15, 0.85); root.setCenter(mainSplit); + if (project.hasFacet(FacetManager.GRADLE)) { + openGradleTab(project, project.getFacet(FacetManager.GRADLE).orElseThrow(), rightPane, root, mainSplit); + } + + Railroad.EVENT_BUS.subscribe(FacetDetectedEvent.class, event -> { + if (event.project() != project) + return; + + openGradleTab(project, event.facet(), rightPane, root, mainSplit); + }); + root.setLeft(PaneIconBarFactory.create( leftPane, mainSplit, @@ -86,14 +104,6 @@ public static Scene createIDEScene(Project project) { Map.of("Project", FontAwesomeSolid.FOLDER.getDescription()) )); - root.setRight(PaneIconBarFactory.create( - rightPane, - mainSplit, - Orientation.VERTICAL, - 2, - Map.of("Properties", FontAwesomeSolid.INFO_CIRCLE.getDescription()) - )); - var bottomBar = new RRVBox(); var bottomIcons = PaneIconBarFactory.create( consolePane, @@ -115,6 +125,24 @@ public static Scene createIDEScene(Project project) { return new Scene(root); } + private static void openGradleTab(Project project, Facet facet, DetachableTabPane rightPane, RRBorderPane root, SplitPane mainSplit) { + Platform.runLater(() -> { + if (facet.getType() == FacetManager.GRADLE) { + if (rightPane.getTabs().stream().noneMatch(tab -> tab.getContent() instanceof GradleToolsPane)) { + rightPane.addTab("Gradle", new GradleToolsPane(project)); + + root.setRight(PaneIconBarFactory.create( + rightPane, + mainSplit, + Orientation.VERTICAL, + 2, + Map.of("Gradle", RailroadBrandsIcon.GRADLE.getDescription()) + )); + } + } + }); + } + public static void showEditRunConfigurationsWindow(@NotNull Project project, @Nullable RunConfiguration runConfiguration) { var editorPane = new RunConfigurationEditorPane(project); WindowBuilder.create() diff --git a/src/main/java/dev/railroadide/railroad/ide/runconfig/RunConfigurationManager.java b/src/main/java/dev/railroadide/railroad/ide/runconfig/RunConfigurationManager.java index 37627f9f..9c333c61 100644 --- a/src/main/java/dev/railroadide/railroad/ide/runconfig/RunConfigurationManager.java +++ b/src/main/java/dev/railroadide/railroad/ide/runconfig/RunConfigurationManager.java @@ -5,6 +5,8 @@ import dev.railroadide.railroad.Railroad; import dev.railroadide.railroad.project.Project; import dev.railroadide.railroad.project.data.ProjectDataStore; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -21,6 +23,9 @@ public class RunConfigurationManager { @Getter private final ObservableList> configurations = FXCollections.observableArrayList(); + @Getter + private final ObjectProperty> selectedConfiguration = new SimpleObjectProperty<>(); + private final Project project; public RunConfigurationManager(Project project) { @@ -31,6 +36,15 @@ public RunConfigurationManager(Project project) { (ListChangeListener>) change -> writeRunConfigurations()); } + /** + * Set the selected run configuration. + * + * @param configuration The run configuration to select, or null to clear the selection. + */ + public void setSelectedConfiguration(@Nullable RunConfiguration configuration) { + this.selectedConfiguration.set(configuration); + } + /** * Remove a run configuration from this project. * diff --git a/src/main/java/dev/railroadide/railroad/ide/runconfig/defaults/data/GradleRunConfigurationData.java b/src/main/java/dev/railroadide/railroad/ide/runconfig/defaults/data/GradleRunConfigurationData.java index 74d20122..594ccba7 100644 --- a/src/main/java/dev/railroadide/railroad/ide/runconfig/defaults/data/GradleRunConfigurationData.java +++ b/src/main/java/dev/railroadide/railroad/ide/runconfig/defaults/data/GradleRunConfigurationData.java @@ -3,9 +3,6 @@ import dev.railroadide.core.form.*; import dev.railroadide.railroad.Railroad; import dev.railroadide.railroad.gradle.model.GradleBuildModel; -import dev.railroadide.railroad.gradle.model.GradleProjectModel; -import dev.railroadide.railroad.gradle.model.task.GradleTaskArgument; -import dev.railroadide.railroad.gradle.model.task.GradleTaskModel; import dev.railroadide.railroad.gradle.service.GradleModelService; import dev.railroadide.railroad.ide.runconfig.RunConfiguration; import dev.railroadide.railroad.ide.runconfig.RunConfigurationData; @@ -17,6 +14,9 @@ import dev.railroadide.railroad.project.onboarding.ProjectValidators; import dev.railroadide.railroad.settings.ui.DetectedJdkListPane; import dev.railroadide.railroad.utility.StringUtils; +import dev.railroadide.railroadplugin.dto.RailroadGradleTask; +import dev.railroadide.railroadplugin.dto.RailroadGradleTaskArgument; +import dev.railroadide.railroadplugin.dto.RailroadModule; import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -28,6 +28,7 @@ import javafx.util.Callback; import lombok.Data; import lombok.EqualsAndHashCode; +import org.gradle.tooling.model.DomainObjectSet; import org.jetbrains.annotations.Nullable; import java.nio.file.Files; @@ -60,7 +61,7 @@ public Form createConfigurationForm(Project project, RunConfiguration configu } }); - ObservableMap> gradleTasksCache = FXCollections.observableHashMap(); + ObservableMap> gradleTasksCache = FXCollections.observableHashMap(); ObjectProperty gradleProjectPathProperty = new SimpleObjectProperty<>(this.gradleProjectPath); gradleProjectPathProperty.addListener((observable, oldValue, newValue) -> loadGradleTasksAsync(project, newValue, gradleTasksCache)); @@ -139,29 +140,29 @@ public Form createConfigurationForm(Project project, RunConfiguration configu ).build(); } - private List buildGradleTaskSuggestions(Path gradleProjectPath, ObservableMap> gradleTasksCache) { + private List buildGradleTaskSuggestions(Path gradleProjectPath, ObservableMap> gradleTasksCache) { if (gradleProjectPath == null) return List.of(); - List cachedTasks = gradleTasksCache.get(gradleProjectPath); + List cachedTasks = gradleTasksCache.get(gradleProjectPath); if (cachedTasks == null || cachedTasks.isEmpty()) { Railroad.LOGGER.debug("No cached Gradle tasks for {} yet", gradleProjectPath); return List.of(); } LinkedHashSet suggestions = new LinkedHashSet<>(); - for (GradleTaskModel task : cachedTasks) { + for (RailroadGradleTask task : cachedTasks) { if (task == null) continue; - String taskName = task.name(); + String taskName = task.getName(); if (taskName != null && !taskName.isBlank()) suggestions.add(taskName); - List options = task.arguments(); + DomainObjectSet options = task.getArguments(); if (options != null && !options.isEmpty()) { options.stream() - .map(GradleTaskArgument::name) + .map(RailroadGradleTaskArgument::getName) .filter(name -> name != null && !name.isBlank()) .forEach(suggestions::add); } @@ -173,7 +174,7 @@ private List buildGradleTaskSuggestions(Path gradleProjectPath, Observab } private Collection filterGradleTaskSuggestions(String query, Path gradleProjectPath, - ObservableMap> gradleTasksCache) { + ObservableMap> gradleTasksCache) { List suggestions = buildGradleTaskSuggestions(gradleProjectPath, gradleTasksCache); String token = currentToken(query); if (token.isBlank()) { @@ -203,7 +204,7 @@ private String currentToken(String text) { private Callback, ListCell> createGradleTaskSuggestionCellFactory( ObjectProperty gradleProjectPathProperty, - ObservableMap> gradleTasksCache) { + ObservableMap> gradleTasksCache) { return listView -> new ListCell<>() { @Override protected void updateItem(String item, boolean empty) { @@ -224,31 +225,31 @@ protected void updateItem(String item, boolean empty) { } private @Nullable String findGradleTaskDescription(String taskOrOptionName, Path gradleProjectPath, - ObservableMap> gradleTasksCache) { + ObservableMap> gradleTasksCache) { if (taskOrOptionName == null || gradleProjectPath == null) return null; - List tasks = gradleTasksCache.get(gradleProjectPath); + List tasks = gradleTasksCache.get(gradleProjectPath); if (tasks == null || tasks.isEmpty()) return null; - for (GradleTaskModel task : tasks) { + for (RailroadGradleTask task : tasks) { if (task == null) continue; - if (taskOrOptionName.equals(task.name())) { - String description = task.description(); + if (taskOrOptionName.equals(task.getName())) { + String description = task.getDescription(); return description == null || description.isBlank() ? null : description; } - List arguments = task.arguments(); + DomainObjectSet arguments = task.getArguments(); if (arguments != null && !arguments.isEmpty()) { - for (GradleTaskArgument argument : arguments) { + for (RailroadGradleTaskArgument argument : arguments) { if (argument == null) continue; - if (taskOrOptionName.equals(argument.name())) { - String description = argument.description(); + if (taskOrOptionName.equals(argument.getName())) { + String description = argument.getDescription(); if (description != null && !description.isBlank()) return description; } @@ -259,7 +260,7 @@ protected void updateItem(String item, boolean empty) { return null; } - private void loadGradleTasksAsync(Project project, Path gradleProjectPath, ObservableMap> gradleTasksCache) { + private void loadGradleTasksAsync(Project project, Path gradleProjectPath, ObservableMap> gradleTasksCache) { if (gradleProjectPath == null) { if (Platform.isFxApplicationThread()) { gradleTasksCache.clear(); @@ -297,25 +298,28 @@ private static Path normalizeGradleProjectPath(String rawValue) { } } - private List fetchTasksForProject(Project project, Path gradleProjectPath) { + private List fetchTasksForProject(Project project, Path gradleProjectPath) { try { GradleModelService modelService = project.getGradleManager().getGradleModelService(); GradleBuildModel model = modelService.refreshModel(false).get(); - if (model == null || model.projects() == null) + if (model == null) return List.of(); - for (GradleProjectModel projectModel : model.projects()) { - if (projectModel == null) + for (RailroadModule module : model.project().getModules()) { + if (module == null) continue; - if (gradleProjectPath.equals(projectModel.projectDir())) - return projectModel.tasks() != null ? projectModel.tasks() : List.of(); + if (gradleProjectPath.equals(module.getGradleProject().getProjectDirectory().toPath())) { + DomainObjectSet tasks = module.getTasks(); + return tasks != null ? tasks.stream().toList() : List.of(); + } } - return model.projects().stream() - .map(GradleProjectModel::tasks) + return model.project().getModules().stream() + .flatMap(module -> module.getTasks().stream()) .filter(Objects::nonNull) .findFirst() + .map(List::of) .orElse(List.of()); } catch (Exception exception) { Railroad.LOGGER.warn("Failed to fetch Gradle tasks for {}", gradleProjectPath, exception); diff --git a/src/main/java/dev/railroadide/railroad/ide/runconfig/ui/RunConfigurationEditorPane.java b/src/main/java/dev/railroadide/railroad/ide/runconfig/ui/RunConfigurationEditorPane.java index 9679817b..a4ce5864 100644 --- a/src/main/java/dev/railroadide/railroad/ide/runconfig/ui/RunConfigurationEditorPane.java +++ b/src/main/java/dev/railroadide/railroad/ide/runconfig/ui/RunConfigurationEditorPane.java @@ -60,6 +60,7 @@ public RunConfigurationEditorPane(Project project) { this.centerContentContainer = new StackPane(); this.editorSplitPane = createEditorSplitPane(); this.selectedConfiguration.bindBidirectional(configurationTreeView.selectedConfigurationProperty()); + this.selectedConfiguration.bindBidirectional(project.getRunConfigManager().getSelectedConfiguration()); getStyleClass().add("run-configuration-editor-pane"); initializeUI(); diff --git a/src/main/java/dev/railroadide/railroad/java/JDK.java b/src/main/java/dev/railroadide/railroad/java/JDK.java index 2ccaebc5..3dd45133 100644 --- a/src/main/java/dev/railroadide/railroad/java/JDK.java +++ b/src/main/java/dev/railroadide/railroad/java/JDK.java @@ -20,14 +20,14 @@ * Java version, and brand (e.g., Oracle, Adoptium). It also provides utility methods * for interacting with the JDK's command-line interface tools. */ -@ToString +@ToString(exclude = "cli") @EqualsAndHashCode public final class JDK { private final Path path; private final String name; private final JavaVersion version; private final Brand brand; - private final JDKCLI cli; + private transient final JDKCLI cli; /** * Constructs a new {@code JDK} instance with the specified path, name, version, and brand. diff --git a/src/main/java/dev/railroadide/railroad/project/FacetDetectedEvent.java b/src/main/java/dev/railroadide/railroad/project/FacetDetectedEvent.java new file mode 100644 index 00000000..9234a565 --- /dev/null +++ b/src/main/java/dev/railroadide/railroad/project/FacetDetectedEvent.java @@ -0,0 +1,7 @@ +package dev.railroadide.railroad.project; + +import dev.railroadide.railroad.project.facet.Facet; +import dev.railroadide.railroadpluginapi.event.Event; + +public record FacetDetectedEvent(Project project, Facet facet) implements Event { +} diff --git a/src/main/java/dev/railroadide/railroad/project/Project.java b/src/main/java/dev/railroadide/railroad/project/Project.java index 5843c7df..628459c4 100644 --- a/src/main/java/dev/railroadide/railroad/project/Project.java +++ b/src/main/java/dev/railroadide/railroad/project/Project.java @@ -137,6 +137,7 @@ private void discoverFacets() { for (Facet facet : facets) { if (facet != null) { this.facets.add(facet); + Railroad.EVENT_BUS.publish(new FacetDetectedEvent(this, facet)); } else { Railroad.LOGGER.warn("Discovered null facet for project: {}", getPathString()); } diff --git a/src/main/java/dev/railroadide/railroad/project/facet/FacetDetector.java b/src/main/java/dev/railroadide/railroad/project/facet/FacetDetector.java index 61242c6d..ef33b44f 100644 --- a/src/main/java/dev/railroadide/railroad/project/facet/FacetDetector.java +++ b/src/main/java/dev/railroadide/railroad/project/facet/FacetDetector.java @@ -1,8 +1,7 @@ package dev.railroadide.railroad.project.facet; -import org.jetbrains.annotations.NotNull; +import dev.railroadide.railroad.project.Project; -import java.nio.file.Path; import java.util.Optional; /** @@ -16,8 +15,8 @@ public interface FacetDetector { /** * Detects a facet based on the provided path. * - * @param path the path to analyze for facet detection + * @param project the project context for detection * @return an {@link Optional} containing the detected facet if found, or an {@link Optional#empty} if no facet is detected */ - Optional> detect(@NotNull Path path); + Optional> detect(Project project); } diff --git a/src/main/java/dev/railroadide/railroad/project/facet/FacetManager.java b/src/main/java/dev/railroadide/railroad/project/facet/FacetManager.java index 9f199af4..de86d6dc 100644 --- a/src/main/java/dev/railroadide/railroad/project/facet/FacetManager.java +++ b/src/main/java/dev/railroadide/railroad/project/facet/FacetManager.java @@ -12,8 +12,6 @@ import dev.railroadide.railroad.project.facet.detector.MavenFacetDetector; import org.jetbrains.annotations.NotNull; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -148,32 +146,16 @@ public static FacetType registerFacet(@NotNull FacetType facetType, @N } public static CompletableFuture>> scan(@NotNull Project project) { - if (project == null) - throw new IllegalArgumentException("Project must not be null"); - - return scan(project.getPath()); - } - - public static CompletableFuture>> scan(@NotNull Path projectPath) { - if (projectPath == null) - throw new IllegalArgumentException("Project path must not be null"); - - if (Files.notExists(projectPath)) - throw new IllegalArgumentException("Project path does not exist: " + projectPath); - - if (!Files.isDirectory(projectPath)) - throw new IllegalArgumentException("Project path must be a directory: " + projectPath); - return CompletableFuture.supplyAsync(() -> { Set> facets = DETECTORS.stream() - .map(detector -> detector.detect(projectPath)) + .map(detector -> detector.detect(project)) .flatMap(Optional::stream) .collect(Collectors.toSet()); if (facets.isEmpty()) { - Railroad.LOGGER.warn("No facets detected for project at {}", projectPath); + Railroad.LOGGER.warn("No facets detected for project at {}", project.getPath()); } else { - Railroad.LOGGER.info("Detected {} facets for project at {}", facets.size(), projectPath); + Railroad.LOGGER.info("Detected {} facets for project at {}", facets.size(), project.getPath()); } return facets; diff --git a/src/main/java/dev/railroadide/railroad/project/facet/data/FabricFacetData.java b/src/main/java/dev/railroadide/railroad/project/facet/data/FabricFacetData.java index 236d7706..b4017343 100644 --- a/src/main/java/dev/railroadide/railroad/project/facet/data/FabricFacetData.java +++ b/src/main/java/dev/railroadide/railroad/project/facet/data/FabricFacetData.java @@ -26,4 +26,8 @@ public class FabricFacetData extends MinecraftModFacetData { * The version of Loom used by the project. */ private String loomVersion; + /** + * Whether the project is using Architectury Loom. + */ + private boolean isArchitecturyLoom; } diff --git a/src/main/java/dev/railroadide/railroad/project/facet/data/GradleFacetData.java b/src/main/java/dev/railroadide/railroad/project/facet/data/GradleFacetData.java index 205076f2..e30f6198 100644 --- a/src/main/java/dev/railroadide/railroad/project/facet/data/GradleFacetData.java +++ b/src/main/java/dev/railroadide/railroad/project/facet/data/GradleFacetData.java @@ -2,22 +2,7 @@ import lombok.Data; -/** - * Holds information about the Gradle build configuration for a project facet. - * Used by the Gradle facet to describe the build file, version, and script type. - */ @Data public class GradleFacetData { - /** - * The version of Gradle used by the project, if detected. - */ - private String gradleVersion; - /** - * The path to the Gradle build file (build.gradle or build.gradle.kts). - */ - private String buildFilePath; - /** - * True if the build file is a Kotlin script (build.gradle.kts), false if Groovy (build.gradle). - */ - private boolean isKts; + } diff --git a/src/main/java/dev/railroadide/railroad/project/facet/detector/FabricFacetDetector.java b/src/main/java/dev/railroadide/railroad/project/facet/detector/FabricFacetDetector.java index b06f4071..8efe96cb 100644 --- a/src/main/java/dev/railroadide/railroad/project/facet/detector/FabricFacetDetector.java +++ b/src/main/java/dev/railroadide/railroad/project/facet/detector/FabricFacetDetector.java @@ -1,26 +1,21 @@ package dev.railroadide.railroad.project.facet.detector; import com.google.gson.JsonObject; -import dev.railroadide.fabricExtractorPlugin.model.FabricExtractorModel; -import dev.railroadide.railroad.AppResources; import dev.railroadide.railroad.Railroad; +import dev.railroadide.railroad.gradle.model.GradleBuildModel; +import dev.railroadide.railroad.gradle.service.GradleModelService; +import dev.railroadide.railroad.project.Project; import dev.railroadide.railroad.project.facet.Facet; import dev.railroadide.railroad.project.facet.FacetDetector; import dev.railroadide.railroad.project.facet.FacetManager; import dev.railroadide.railroad.project.facet.data.FabricFacetData; +import dev.railroadide.railroadplugin.dto.FabricDataModel; import org.gradle.api.GradleException; import org.gradle.tooling.BuildException; -import org.gradle.tooling.GradleConnector; -import org.gradle.tooling.ProjectConnection; -import org.jetbrains.annotations.NotNull; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.util.Objects; import java.util.Optional; /** @@ -28,32 +23,15 @@ * This detector is used by the facet system to identify Fabric mod projects and extract relevant configuration data. */ public class FabricFacetDetector implements FacetDetector { - /** - * Extracts the Gradle init script for Fabric metadata extraction to a temporary file. - * - * @return the path to the extracted init script - * @throws IOException if the script cannot be extracted - */ - private static Path extractInitScript() throws IOException { - try (InputStream inputStream = AppResources.getResourceAsStream("scripts/init-fabric-extractor.gradle")) { - if (inputStream == null) - throw new IllegalStateException("init script resource missing"); - - Path tempFile = Files.createTempFile("init-fabric-extractor", ".gradle"); - Files.copy(inputStream, tempFile, StandardCopyOption.REPLACE_EXISTING); - tempFile.toFile().deleteOnExit(); - return tempFile; - } - } - /** * Detects a Fabric facet in the given path by searching for fabric.mod.json and extracting mod metadata and build info. * - * @param path the project directory to analyze + * @param project the project context for detection * @return an Optional containing the Fabric facet if detected, or empty if not found */ @Override - public Optional> detect(@NotNull Path path) { + public Optional> detect(Project project) { + Path path = project.getPath(); Path fabricModJson = path.resolve("src").resolve("main").resolve("resources").resolve("fabric.mod.json"); if (Files.notExists(fabricModJson) || !Files.isRegularFile(fabricModJson) || !Files.isReadable(fabricModJson)) return Optional.empty(); @@ -76,42 +54,20 @@ public Optional> detect(@NotNull Path path) { data.setIssuesUrl(contact.has("issues") ? contact.get("issues").getAsString() : ""); data.setChangelogUrl(json.has("changelog") ? json.get("changelog").getAsString() : ""); - Path buildFilePath = null; - for (String buildFile : GradleFacetDetector.BUILD_FILES) { - Path tryBuildFilePath = path.resolve(buildFile); - if (Files.exists(tryBuildFilePath) && Files.isRegularFile(tryBuildFilePath) && Files.isReadable(tryBuildFilePath)) { - buildFilePath = tryBuildFilePath; - break; + GradleModelService gradleModelService = project.getGradleManager().getGradleModelService(); + gradleModelService.getCachedModel().ifPresent(gradleBuildModel -> { + FabricDataModel fabricDataModel = gradleBuildModel.fabricData(); + data.setMinecraftVersion(fabricDataModel.minecraftVersion()); + data.setYarnMappingsVersion(fabricDataModel.mappingsVersion()); + data.setFabricLoaderVersion(fabricDataModel.loaderVersion()); + data.setFabricApiVersion(fabricDataModel.fabricApiVersion()); + if (fabricDataModel.loomVersion() != null) { + data.setLoomVersion(fabricDataModel.loomVersion().version()); + data.setArchitecturyLoom(fabricDataModel.loomVersion().isArchitecturyLoom()); } - } - - data.setBuildFilePath(Objects.toString(buildFilePath)); - - String minecraftVersion, fabricLoaderVersion, fabricApiVersion, yarnMappingsVersion, loomVersion; - try (ProjectConnection connection = GradleConnector.newConnector() - .forProjectDirectory(path.toFile()) - .connect()) { - Path initScriptPath = extractInitScript(); - - OutputStream outputStream = OutputStream.nullOutputStream(); - FabricExtractorModel model = connection.model(FabricExtractorModel.class) - .withArguments("--init-script", initScriptPath.toAbsolutePath().toString()) - .setStandardOutput(outputStream) - .setStandardError(outputStream) - .get(); - - minecraftVersion = model.minecraftVersion(); - fabricLoaderVersion = model.loaderVersion(); - fabricApiVersion = model.fabricApiVersion(); - yarnMappingsVersion = model.mappingsVersion(); - loomVersion = model.loomVersion(); - } - data.setMinecraftVersion(minecraftVersion); - data.setFabricLoaderVersion(fabricLoaderVersion); - data.setFabricApiVersion(fabricApiVersion); - data.setYarnMappingsVersion(yarnMappingsVersion); - data.setLoomVersion(loomVersion); + // TODO: Set source file + }); return Optional.of(new Facet<>(FacetManager.FABRIC, data)); } catch (IOException exception) { diff --git a/src/main/java/dev/railroadide/railroad/project/facet/detector/GradleFacetDetector.java b/src/main/java/dev/railroadide/railroad/project/facet/detector/GradleFacetDetector.java index a47ecb7b..dde04337 100644 --- a/src/main/java/dev/railroadide/railroad/project/facet/detector/GradleFacetDetector.java +++ b/src/main/java/dev/railroadide/railroad/project/facet/detector/GradleFacetDetector.java @@ -1,11 +1,10 @@ package dev.railroadide.railroad.project.facet.detector; -import dev.railroadide.railroad.Railroad; +import dev.railroadide.railroad.project.Project; import dev.railroadide.railroad.project.facet.Facet; import dev.railroadide.railroad.project.facet.FacetDetector; import dev.railroadide.railroad.project.facet.FacetManager; import dev.railroadide.railroad.project.facet.data.GradleFacetData; -import org.jetbrains.annotations.NotNull; import java.nio.file.Files; import java.nio.file.Path; @@ -22,74 +21,19 @@ public class GradleFacetDetector implements FacetDetector { /** * Detects a Gradle facet in the given path by searching for build.gradle or build.gradle.kts files and reading Gradle version info. * - * @param path the project directory to analyze + * @param project the project to inspect * @return an Optional containing the Gradle facet if detected, or empty if not found */ @Override - public Optional> detect(@NotNull Path path) { + public Optional> detect(Project project) { for (String buildFile : BUILD_FILES) { - Path buildFilePath = path.resolve(buildFile); + Path buildFilePath = project.getPath().resolve(buildFile); if (Files.exists(buildFilePath)) { - var data = new GradleFacetData(); - String buildFilePathStr = buildFilePath.toString(); - boolean isKts = buildFile.endsWith(".kts"); - String gradleVersion; - Optional wrapperProperties = findWrapperProperties(path); - if (wrapperProperties.isPresent()) { - try { - List lines = Files.readAllLines(wrapperProperties.get()); - gradleVersion = parseGradleVersion(lines); - } catch (Exception exception) { - Railroad.LOGGER.error("Error reading gradle-wrapper.properties", exception); - gradleVersion = null; - } - } else { - gradleVersion = null; - } - - data.setGradleVersion(gradleVersion); - data.setBuildFilePath(buildFilePathStr); - data.setKts(isKts); - + var data = new GradleFacetData(); // TODO: Come back to this and figure out what data it should store return Optional.of(new Facet<>(FacetManager.GRADLE, data)); } } return Optional.empty(); } - - /** - * Finds the gradle-wrapper.properties file in the project directory, if present. - * - * @param path the project directory - * @return an Optional containing the path to gradle-wrapper.properties, or empty if not found - */ - private Optional findWrapperProperties(@NotNull Path path) { - Path wrapperProperties = path.resolve("gradle/wrapper/gradle-wrapper.properties"); - if (Files.exists(wrapperProperties)) { - return Optional.of(wrapperProperties); - } - - return Optional.empty(); - } - - /** - * Parses the Gradle version from the lines of a gradle-wrapper.properties file. - * - * @param lines the lines of the properties file - * @return the Gradle version string, or null if not found - */ - private String parseGradleVersion(List lines) { - for (String line : lines) { - if (line.startsWith("distributionUrl=")) { - String url = line.substring("distributionUrl=".length()); - String[] parts = url.split("/"); - if (parts.length > 1) { - return parts[parts.length - 1].replace("gradle-", "").replace(".zip", ""); - } - } - } - - return null; - } } diff --git a/src/main/java/dev/railroadide/railroad/project/facet/detector/JavaFacetDetector.java b/src/main/java/dev/railroadide/railroad/project/facet/detector/JavaFacetDetector.java index f96bf01c..970fc08e 100644 --- a/src/main/java/dev/railroadide/railroad/project/facet/detector/JavaFacetDetector.java +++ b/src/main/java/dev/railroadide/railroad/project/facet/detector/JavaFacetDetector.java @@ -1,8 +1,7 @@ package dev.railroadide.railroad.project.facet.detector; -import dev.railroadide.javaVersionExtractorPlugin.model.JavaVersionModel; -import dev.railroadide.railroad.AppResources; import dev.railroadide.railroad.Railroad; +import dev.railroadide.railroad.project.Project; import dev.railroadide.railroad.project.facet.Facet; import dev.railroadide.railroad.project.facet.FacetDetector; import dev.railroadide.railroad.project.facet.FacetManager; @@ -14,19 +13,13 @@ import org.apache.maven.model.building.*; import org.codehaus.plexus.configuration.PlexusConfigurationException; import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration; -import org.gradle.api.GradleException; -import org.gradle.tooling.BuildException; -import org.gradle.tooling.GradleConnector; -import org.gradle.tooling.ProjectConnection; +import org.gradle.tooling.model.java.InstalledJdk; import org.jetbrains.annotations.NotNull; import java.io.DataInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -41,14 +34,20 @@ public class JavaFacetDetector implements FacetDetector { * Attempts to determine the most reliable Java version for the given project path. * Checks Gradle, Maven, compiled class files, and system properties in order. * - * @param path the project directory + * @param project the project * @return the detected JavaVersion, or an invalid version if not found */ - private static JavaVersion findMostReliableJavaVersion(@NotNull Path path) { - JavaVersion gradleVersion = getJavaVersionFromGradle(path); + private static JavaVersion findMostReliableJavaVersion(@NotNull Project project) { + JavaVersion gradleVersion = getJavaVersionFromGradle(project); if (gradleVersion.major() != -1) return gradleVersion; + Path path = project.getPath(); + if (path == null) { + Railroad.LOGGER.warn("Project path is null for project: {}", project.getAlias()); + return JavaVersion.fromMajor(-1); + } + JavaVersion mavenVersion = getJavaVersionFromMaven(path); if (mavenVersion.major() != -1) return mavenVersion; @@ -63,7 +62,7 @@ private static JavaVersion findMostReliableJavaVersion(@NotNull Path path) { return systemVersion; Railroad.LOGGER.warn("No reliable Java version found for path: {}", path); - return JavaVersion.fromMajor(-1); // Fallback to an invalid version + return JavaVersion.fromMajor(-1); } /** @@ -124,57 +123,24 @@ private static JavaVersion parseJavaVersionFromClassFile(@NotNull Path classFile /** * Attempts to extract the Java version from a Gradle project by connecting to the build and reading configuration. * - * @param path the project directory + * @param project the project * @return the JavaVersion specified in the Gradle build, or an invalid version if not found */ - private static JavaVersion getJavaVersionFromGradle(@NotNull Path path) { - boolean hasBuildFile = false; - for (String buildFile : GradleFacetDetector.BUILD_FILES) { - Path tryBuildFilePath = path.resolve(buildFile); - if (Files.exists(tryBuildFilePath) && Files.isRegularFile(tryBuildFilePath) && Files.isReadable(tryBuildFilePath)) { - hasBuildFile = true; - break; - } - } - - if (!hasBuildFile) - return JavaVersion.fromMajor(-1); // No Gradle build file found - - try (ProjectConnection connection = GradleConnector.newConnector() - .forProjectDirectory(path.toFile()) - .connect()) { - if (connection == null) - return JavaVersion.fromMajor(-1); // No Gradle connection - - Path initScriptPath = extractInitScript(); - - OutputStream outputStream = OutputStream.nullOutputStream(); - JavaVersionModel model = connection.model(JavaVersionModel.class) - .withArguments("--init-script", initScriptPath.toAbsolutePath().toString()) - .setStandardOutput(outputStream) - .setStandardError(outputStream) - .get(); - if (model == null) { - Railroad.LOGGER.warn("No Java version model found in Gradle project at path: {}", path); - return JavaVersion.fromMajor(-1); - } - - JavaVersion sourceVersion = JavaVersion.fromReleaseString(model.sourceCompatibility()); - JavaVersion targetVersion = JavaVersion.fromReleaseString(model.targetCompatibility()); - System.out.println("Source compatibility: " + sourceVersion); - System.out.println("Target compatibility: " + targetVersion); - - return sourceVersion.compareTo(targetVersion) >= 0 ? - sourceVersion : - targetVersion; - } catch (IOException exception) { - Railroad.LOGGER.error("IO exception while detecting Java version in path: {}", path, exception); - } catch (GradleException | BuildException ignored) { - } catch (Exception exception) { - Railroad.LOGGER.error("Unexpected error while detecting Java version in path: {}", path, exception); - } + private static JavaVersion getJavaVersionFromGradle(@NotNull Project project) { + if (!project.getGradleManager().isGradleProject()) + return JavaVersion.fromMajor(-1); - return JavaVersion.fromMajor(-1); + return project.getGradleManager().getGradleModelService().getCachedModel() + .map(gradleBuildModel -> { + try { + InstalledJdk jdk = gradleBuildModel.project().javaLanguageSettings().getJdk(); + return JavaVersion.fromMajor(Integer.parseInt(jdk.getJavaVersion().getMajorVersion())); + } catch (NumberFormatException exception) { + Railroad.LOGGER.error("Error parsing Java version from Gradle model for project: {}", project.getAlias(), exception); + return JavaVersion.fromMajor(-1); + } + }) + .orElse(JavaVersion.fromMajor(-1)); } /** @@ -238,50 +204,28 @@ private static JavaVersion getJavaVersionFromMaven(Path projectDir) { } } - /** - * Extracts the Gradle init script for Java version detection to a temporary file. - * - * @return the path to the extracted init script - * @throws IOException if the script cannot be extracted - */ - private static Path extractInitScript() throws IOException { - try (InputStream inputStream = AppResources.getResourceAsStream("scripts/init-java-version.gradle")) { - if (inputStream == null) - throw new IllegalStateException("init script resource missing"); - - Path tempFile = Files.createTempFile("init-java-version", ".gradle"); - Files.copy(inputStream, tempFile, StandardCopyOption.REPLACE_EXISTING); - tempFile.toFile().deleteOnExit(); - return tempFile; - } - } - /** * Detects a Java facet in the given path by searching for .java files and determining the Java version. * - * @param path the project directory or file to analyze + * @param project the project * @return an Optional containing the Java facet if detected, or empty if not found */ @Override - public Optional> detect(@NotNull Path path) { + public Optional> detect(Project project) { long javaFileCount = 0; try { - if (Files.isDirectory(path)) { - try (Stream javaFiles = Files.find(path, 10, - (p, attrs) -> p.toString().endsWith(".java"))) { - javaFileCount = javaFiles.count(); - } - } else if (path.toString().endsWith(".java")) { - javaFileCount = 1; + try (Stream javaFiles = Files.find(project.getPath(), 10, + (p, attrs) -> p.toString().endsWith(".java"))) { + javaFileCount = javaFiles.count(); } } catch (IOException exception) { - Railroad.LOGGER.error("Error while detecting Java files in path: {}", path, exception); + Railroad.LOGGER.error("Error while detecting Java files in path: {}", project.getPath(), exception); } JavaFacetData data = null; if (javaFileCount > 0) { data = new JavaFacetData(); - JavaVersion highestJavaVersion = findMostReliableJavaVersion(path); + JavaVersion highestJavaVersion = findMostReliableJavaVersion(project); data.setVersion(highestJavaVersion); } diff --git a/src/main/java/dev/railroadide/railroad/project/facet/detector/MavenFacetDetector.java b/src/main/java/dev/railroadide/railroad/project/facet/detector/MavenFacetDetector.java index 16633224..0f0fe231 100644 --- a/src/main/java/dev/railroadide/railroad/project/facet/detector/MavenFacetDetector.java +++ b/src/main/java/dev/railroadide/railroad/project/facet/detector/MavenFacetDetector.java @@ -1,13 +1,13 @@ package dev.railroadide.railroad.project.facet.detector; import dev.railroadide.railroad.Railroad; +import dev.railroadide.railroad.project.Project; import dev.railroadide.railroad.project.facet.Facet; import dev.railroadide.railroad.project.facet.FacetDetector; import dev.railroadide.railroad.project.facet.FacetManager; import dev.railroadide.railroad.project.facet.data.MavenFacetData; import org.apache.maven.model.Model; import org.apache.maven.model.building.*; -import org.jetbrains.annotations.NotNull; import java.nio.file.Files; import java.nio.file.Path; @@ -23,12 +23,12 @@ public class MavenFacetDetector implements FacetDetector { /** * Detects a Maven facet in the given path by searching for pom.xml and extracting Maven coordinates. * - * @param path the project directory to analyze + * @param project the project to inspect * @return an Optional containing the Maven facet if detected, or empty if not found */ @Override - public Optional> detect(@NotNull Path path) { - Path pomFile = path.resolve("pom.xml"); + public Optional> detect(Project project) { + Path pomFile = project.getPath().resolve("pom.xml"); if (Files.notExists(pomFile) || !Files.isRegularFile(pomFile) || !Files.isReadable(pomFile)) return Optional.empty(); diff --git a/src/main/java/dev/railroadide/railroad/utility/StringUtils.java b/src/main/java/dev/railroadide/railroad/utility/StringUtils.java index f27b9c47..94f20cb1 100644 --- a/src/main/java/dev/railroadide/railroad/utility/StringUtils.java +++ b/src/main/java/dev/railroadide/railroad/utility/StringUtils.java @@ -214,4 +214,22 @@ public static String[] stringToStringArray(String str, String delimiter) { return str.split(delimiter); } + + public static String capitalizeFirstLetterOfEachWord(String input) { + String[] words = input.split(" "); + var capitalized = new StringBuilder(); + for (int i = 0; i < words.length; i++) { + String word = words[i]; + if (!word.isEmpty()) { + capitalized.append(Character.toUpperCase(word.charAt(0))) + .append(word.substring(1).toLowerCase()); + + if (i < words.length - 1) { + capitalized.append(" "); + } + } + } + + return capitalized.toString().trim(); + } } diff --git a/src/main/resources/assets/railroad/lang/en_us.lang b/src/main/resources/assets/railroad/lang/en_us.lang index 8e7bf29b..cb9cedbd 100644 --- a/src/main/resources/assets/railroad/lang/en_us.lang +++ b/src/main/resources/assets/railroad/lang/en_us.lang @@ -150,6 +150,16 @@ railroad.runconfig.shell_script.configuration.interpreterArgs.label=Interpreter railroad.runconfig.shell_script.configuration.interpreterArgs.prompt=Arguments separated by spaces railroad.runconfig.shell_script.configuration.executeInTerminal.label=Run in terminal +# ============================================================================= +# Gradle Tools +# ============================================================================= +railroad.gradle.tools.tasks=Tasks +railroad.gradle.tools.dependencies=Dependencies +railroad.gradle.tools.button.sync.tooltip=Sync Gradle Project +railroad.gradle.tools.button.downloadsources.tooltip=Download Sources +railroad.gradle.tools.button.toggleoffline.tooltip=Toggle Offline Mode +railroad.gradle.tools.ctx_menu.open_gradle_config=Open Gradle Configuration +railroad.gradle.tools.ctx_menu.sync=Sync Gradle Project # ============================================================================= # HOME/WELCOME SCREEN diff --git a/src/main/resources/assets/railroad/scripts/init-download-sources.gradle b/src/main/resources/assets/railroad/scripts/init-download-sources.gradle new file mode 100644 index 00000000..e1c97444 --- /dev/null +++ b/src/main/resources/assets/railroad/scripts/init-download-sources.gradle @@ -0,0 +1,32 @@ +initscript { + repositories { + mavenLocal() + maven { url "https://maven.railroadide.dev/releases" } + gradlePluginPortal() + } + dependencies { + classpath "dev.railroadide:RailroadGradlePlugin:1.0.0" + } +} + +beforeSettings { settings -> + // Apply to the actual Settings object so the plugin receives the expected target type. + settings.pluginManager.apply(dev.railroadide.railroadplugin.RailroadSettingsPlugin) +} + +allprojects { + apply plugin: dev.railroadide.railroadplugin.RailroadProjectPlugin +} + +// Aggregate a single root task that triggers downloads for all projects +gradle.projectsLoaded { gradle -> + def root = gradle.rootProject + def aggregate = root.tasks.register("railroadDownloadAllSources") { task -> + task.group = "Documentation" + task.description = "Downloads sources for all projects using Railroad download-sources plugin." + } + + root.allprojects { project -> + aggregate.configure { dependsOn(project.tasks.named("downloadDependencySources")) } + } +} diff --git a/src/main/resources/assets/railroad/scripts/init-fabric-extractor.gradle b/src/main/resources/assets/railroad/scripts/init-fabric-extractor.gradle deleted file mode 100644 index 5ab49ec9..00000000 --- a/src/main/resources/assets/railroad/scripts/init-fabric-extractor.gradle +++ /dev/null @@ -1,34 +0,0 @@ -initscript { - repositories { - mavenLocal() - } - dependencies { - classpath "dev.railroadide.railroad:fabricextractor:1.0.0" - } -} - -def capturedLoomVersion = null - -// 1) During settings evaluation, intercept plugin resolution -gradle.settingsEvaluated { settings -> - settings.pluginManagement { - resolutionStrategy { - eachPlugin { details -> - if (details.requested.id.id == 'fabric-loom') { - capturedLoomVersion = details.requested.version - } - } - } - } -} - -// 2) Once all projects are loaded, stamp that version into each project -gradle.projectsLoaded { rootProject -> - rootProject.allprojects { project -> - if (capturedLoomVersion) { - project.extensions.extraProperties.set('fabricLoomVersion', capturedLoomVersion) - } - // Apply your extractor plugin (on the initscript classpath) - project.plugins.apply dev.railroadide.railroad.fabricextractor.FabricExtractorPlugin - } -} \ No newline at end of file diff --git a/src/main/resources/assets/railroad/scripts/init-gradle-plugin.gradle b/src/main/resources/assets/railroad/scripts/init-gradle-plugin.gradle new file mode 100644 index 00000000..455c9ea2 --- /dev/null +++ b/src/main/resources/assets/railroad/scripts/init-gradle-plugin.gradle @@ -0,0 +1,19 @@ +initscript { + repositories { + mavenLocal() + maven { url "https://maven.railroadide.dev/releases" } + gradlePluginPortal() + } + dependencies { + classpath "dev.railroadide:RailroadGradlePlugin:1.0.0" + } +} + +beforeSettings { settings -> + // Apply to the actual Settings object so the plugin receives the expected target type. + settings.pluginManager.apply(dev.railroadide.railroadplugin.RailroadSettingsPlugin) +} + +allprojects { + apply plugin: dev.railroadide.railroadplugin.RailroadProjectPlugin +} diff --git a/src/main/resources/assets/railroad/scripts/init-java-version.gradle b/src/main/resources/assets/railroad/scripts/init-java-version.gradle deleted file mode 100644 index 6d5f9bf9..00000000 --- a/src/main/resources/assets/railroad/scripts/init-java-version.gradle +++ /dev/null @@ -1,13 +0,0 @@ -initscript { - repositories { - mavenLocal() - } - - dependencies { - classpath "dev.railroadide.railroad:javaversion:1.0.0" - } -} - -allprojects { - apply plugin: dev.railroadide.railroad.javaversion.JavaVersionModelPlugin -} \ No newline at end of file diff --git a/src/main/resources/assets/railroad/scripts/init-task-args.gradle b/src/main/resources/assets/railroad/scripts/init-task-args.gradle deleted file mode 100644 index 9ae1270d..00000000 --- a/src/main/resources/assets/railroad/scripts/init-task-args.gradle +++ /dev/null @@ -1,61 +0,0 @@ -import groovy.json.JsonOutput -import org.gradle.api.internal.tasks.options.OptionReader - -def outputPath = System.getProperty("__TASK_ARGS_PROPERTY__") -if (!outputPath) - return - -gradle.projectsEvaluated { - def reader = new OptionReader() - def optionsByTask = [:] - - gradle.rootProject?.allprojects?.each { proj -> - proj.tasks.each { task -> - def collected = [] - try { - reader.getOptions(task).values().each { opt -> - def argType = opt.argumentType - def type = "STRING" - if (argType != null) { - if (argType == Boolean.TYPE || argType == Boolean) { - type = "BOOLEAN" - } else if (argType.isEnum()) { - type = "ENUM" - } else if (argType.isPrimitive()) { - type = "NUMBER" - } else if (Number.isAssignableFrom(argType)) { - type = "NUMBER" - } else if (argType.name in ["java.io.File", "java.nio.file.Path"]) { - type = "FILE" - } - } - - def enumValues = [] - try { - enumValues = (opt.availableValues ?: []).collect { it.toString() } - } catch (Throwable ignored) { - } - - collected.add([ - name : opt.name, - displayName : opt.name, - type : type, - defaultValue: "", - description : opt.description ?: "", - enumValues : enumValues - ]) - } - } catch (Throwable ignored) { - } - - optionsByTask[task.path] = collected - } - } - - try { - def outFile = new File(outputPath) - outFile.parentFile?.mkdirs() - outFile.setText(JsonOutput.toJson(optionsByTask), "UTF-8") - } catch (Throwable ignored) { - } -} diff --git a/src/main/resources/assets/railroad/styles/components/button.css b/src/main/resources/assets/railroad/styles/components/button.css index 3d50b701..fa89a8a9 100644 --- a/src/main/resources/assets/railroad/styles/components/button.css +++ b/src/main/resources/assets/railroad/styles/components/button.css @@ -23,6 +23,12 @@ -fx-translate-y: 1px; } +.rr-button.toggle:selected { + -fx-background-color: -color-accent-7; + -fx-text-fill: -color-fg-emphasis; + -fx-effect: dropshadow(gaussian, rgba(94, 129, 172, 0.35), 5, 0, 0, 2); +} + .rr-button:disabled { -fx-background-color: -color-neutral-muted; -fx-text-fill: -color-fg-muted; @@ -47,6 +53,12 @@ -fx-border-color: -color-accent-emphasis; } +.rr-button.toggle:selected.secondary { + -fx-background-color: -color-bg-inset; + -fx-border-color: -color-accent-emphasis; + -fx-text-fill: -color-fg-default; +} + .rr-button.ghost { -fx-background-color: transparent; -fx-text-fill: -color-fg-default; @@ -57,6 +69,11 @@ -fx-background-color: -color-bg-subtle; } +.rr-button.toggle:selected.ghost { + -fx-background-color: -color-bg-subtle; + -fx-text-fill: -color-fg-default; +} + .rr-button.danger { -fx-background-color: -color-danger-emphasis; -fx-text-fill: -color-fg-emphasis; @@ -66,6 +83,10 @@ -fx-background-color: -color-danger-6; } +.rr-button.toggle:selected.danger { + -fx-background-color: -color-danger-6; +} + .rr-button.success { -fx-background-color: -color-success-emphasis; -fx-text-fill: -color-fg-emphasis; @@ -75,6 +96,10 @@ -fx-background-color: -color-success-6; } +.rr-button.toggle:selected.success { + -fx-background-color: -color-success-6; +} + .rr-button.warning { -fx-background-color: -color-warning-emphasis; -fx-text-fill: -color-fg-emphasis; @@ -84,6 +109,10 @@ -fx-background-color: -color-warning-6; } +.rr-button.toggle:selected.warning { + -fx-background-color: -color-warning-6; +} + /* Button sizes */ .rr-button.small { -fx-min-height: 28px; diff --git a/src/main/resources/assets/railroad/styles/components/gradle-tools.css b/src/main/resources/assets/railroad/styles/components/gradle-tools.css new file mode 100644 index 00000000..2c33c533 --- /dev/null +++ b/src/main/resources/assets/railroad/styles/components/gradle-tools.css @@ -0,0 +1,39 @@ +.gradle-tool-content-pane { + -fx-border-width: 0; +} + +.gradle-project-element { + -fx-icon-color: #26c77c !important; +} + +.gradle-tasks-group-element { + -fx-icon-color: #ddc03a !important; +} + +.gradle-task-element { + -fx-icon-color: #3a7bdd !important; +} + +.sync-button .ikonli-font-icon { + -fx-icon-color: #26c77c !important; +} + +.sync-button:hover .ikonli-font-icon { + -fx-icon-color: #1ea66a !important; +} + +.download-sources-button .ikonli-font-icon { + -fx-icon-color: #3a7bdd !important; +} + +.download-sources-button:hover .ikonli-font-icon { + -fx-icon-color: #2e5fbf !important; +} + +.toggle-offline-mode-button .stacked-ikonli-font-icon { + -fx-icon-color: #ddc03a !important; +} + +.toggle-offline-mode-button:hover .stacked-ikonli-font-icon { + -fx-icon-color: #bfa831 !important; +}