diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/problem/ProblemTracking.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/problem/ProblemTracking.java index 7eb1883a1..5bd875c20 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/problem/ProblemTracking.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/problem/ProblemTracking.java @@ -99,7 +99,7 @@ public void clear() { * @param listener * Listener to add. */ - public void addListener(ProblemInvalidationListener listener) { + public void addListener(@Nonnull ProblemInvalidationListener listener) { listeners.add(listener); } @@ -110,7 +110,7 @@ public void addListener(ProblemInvalidationListener listener) { * @return {@code true} when listener was removed. * {@code false} when listener was not present to begin with. */ - public boolean removeListener(ProblemInvalidationListener listener) { + public boolean removeListener(@Nonnull ProblemInvalidationListener listener) { return listeners.remove(listener); } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/ProblemOverlay.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/ProblemOverlay.java index 7cafacd25..e320c276f 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/ProblemOverlay.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/ProblemOverlay.java @@ -1,19 +1,27 @@ package software.coley.recaf.ui.pane.editing; +import atlantafx.base.controls.Popover; import atlantafx.base.theme.Styles; import jakarta.annotation.Nonnull; +import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ChangeListener; import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Group; +import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.effect.BoxBlur; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import org.fxmisc.richtext.CodeArea; import org.kordamp.ikonli.carbonicons.CarbonIcons; @@ -22,10 +30,13 @@ import software.coley.recaf.ui.control.richtext.Editor; import software.coley.recaf.ui.control.richtext.EditorComponent; import software.coley.recaf.ui.control.richtext.ScrollbarPaddingUtil; +import software.coley.recaf.ui.control.richtext.problem.Problem; import software.coley.recaf.ui.control.richtext.problem.ProblemInvalidationListener; import software.coley.recaf.ui.control.richtext.problem.ProblemLevel; import software.coley.recaf.ui.control.richtext.problem.ProblemTracking; +import java.util.Collection; + /** * Simple problem overlay, showing users how many problems of what type there are in the current {@link Editor}. * @@ -41,13 +52,84 @@ public class ProblemOverlay extends Group implements EditorComponent, ProblemInv */ public ProblemOverlay() { // Display the number of problems, and their type. + Button indicator = new Button(); + indicator.setFocusTraversable(false); + indicator.getStyleClass().addAll(Styles.SMALL, "muted"); + + // On-click to show a list of all problems + indicator.setOnAction(e -> { + ProblemTracking problemTracking = editor.getProblemTracking(); + if (problemTracking == null) return; + + // Create vertical list + VBox content = new VBox(); + ObservableList children = content.getChildren(); + Collection problems = problemTracking.getProblems().values(); + for (Problem problem : problems) { + // Map level to graphic + ProblemLevel level = problem.getLevel(); + Node graphic = switch (level) { + case ERROR -> new FontIconView(CarbonIcons.ERROR, Color.RED); + case WARN -> new FontIconView(CarbonIcons.WARNING_ALT, Color.YELLOW); + default -> new FontIconView(CarbonIcons.INFORMATION, Color.TURQUOISE); + }; + + // Create 'N: Message' layout + // - Exclude line number 'N' when line number is negative + Label messageLabel = new Label(problem.getMessage()); + messageLabel.setTextFill(Color.RED); + messageLabel.setMaxWidth(Integer.MAX_VALUE); + int line = problem.getLine(); + HBox problemBox; + if (line >= 0) { + Label lineLabel = new Label(String.valueOf(line), graphic); + lineLabel.setTextFill(Color.RED); + lineLabel.getStyleClass().add(Styles.TEXT_BOLD); + problemBox = new HBox(lineLabel, messageLabel); + } else { + messageLabel.setGraphic(graphic); + problemBox = new HBox(messageLabel); + } + problemBox.setSpacing(5); + problemBox.setPadding(new Insets(5)); + + // Make on-hover more clearly show which problem is relevant. + // The changing color on-hover also indicates clickable action. + problemBox.setOnMouseEntered(me -> problemBox.getStyleClass().add("background")); + problemBox.setOnMouseExited(me -> problemBox.getStyleClass().remove("background")); + + // When clicked, center the relevant problem. + problemBox.setOnMousePressed(me -> { + CodeArea codeArea = editor.getCodeArea(); + codeArea.moveTo(line - 1, 0); + codeArea.selectLine(); + codeArea.showParagraphAtCenter(codeArea.getCurrentParagraph()); + }); + children.add(problemBox); + } + + BooleanProperty isMouseOver = new SimpleBooleanProperty(true); + ScrollPane scrollWrapper = new ScrollPane(content); + scrollWrapper.maxHeightProperty().bind(editor.heightProperty().multiply(0.8)); + scrollWrapper.prefViewportWidthProperty().bind(editor.widthProperty().multiply(0.8)); + scrollWrapper.setOnMouseEntered(me -> isMouseOver.set(true)); + scrollWrapper.setOnMouseExited(me -> isMouseOver.set(false)); + scrollWrapper.effectProperty().bind(Bindings.when(isMouseOver.not()) + .then(new BoxBlur(5, 5, 1)) + .otherwise((BoxBlur) null)); + Popover popover = new Popover(scrollWrapper); + popover.setArrowLocation(Popover.ArrowLocation.TOP_RIGHT); + popover.opacityProperty().bind(Bindings.when(isMouseOver) + .then(1.0) + .otherwise(0.4)); + popover.show(indicator); + }); + + // Can recycle the same instance with the indicator graphic FontIconView iconGood = new FontIconView(CarbonIcons.CHECKMARK, Color.LAWNGREEN); FontIconView iconInfo = new FontIconView(CarbonIcons.INFORMATION, Color.TURQUOISE); FontIconView iconWarning = new FontIconView(CarbonIcons.WARNING_ALT, Color.YELLOW); FontIconView iconError = new FontIconView(CarbonIcons.ERROR, Color.RED); - Button indicator = new Button(); - indicator.setFocusTraversable(false); - indicator.getStyleClass().addAll(Styles.SMALL, "muted"); indicator.graphicProperty().bind(problemCount.map(size -> { // Skip before linked to an editor. if (editor == null) @@ -73,6 +155,7 @@ public ProblemOverlay() { if (error > 0) wrapper.getChildren().add(new Label(String.valueOf(error), iconError)); return wrapper; })); + BooleanBinding hasProblems = problemCount.greaterThan(0); hasProblems.addListener((ob, had, has) -> { // When there are problems, this is the left-most button. @@ -124,7 +207,7 @@ public ProblemOverlay() { } }); prev.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.CENTER_PILL, Styles.SMALL, "muted"); - next.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.RIGHT_PILL, Styles.SMALL, "muted"); + next.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.RIGHT_PILL, Styles.SMALL, "muted"); prev.setFocusTraversable(false); next.setFocusTraversable(false); HBox buttons = new HBox(prev, next); diff --git a/recaf-ui/src/main/resources/style/tweaks.css b/recaf-ui/src/main/resources/style/tweaks.css index da065a954..2c2b0ff96 100644 --- a/recaf-ui/src/main/resources/style/tweaks.css +++ b/recaf-ui/src/main/resources/style/tweaks.css @@ -55,6 +55,9 @@ .background { -fx-background-color: -color-bg-default; } +.background-light { + -fx-background-color: -color-border-subtle; +} /* Rounds the display of a container by its bg/border colors */ .round-container {