diff --git a/pom.xml b/pom.xml index b84dfa8c..9842bdfa 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,11 @@ javafx-fxml ${javafx.version} + + org.openjfx + javafx-web + ${javafx.version} + org.junit.jupiter diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 3e89b391..e97c674f 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,9 +1,9 @@ module org.example.petshopdesktop { requires javafx.controls; requires javafx.fxml; + requires javafx.web; requires java.sql; requires java.net.http; - requires java.xml; requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.core; requires com.fasterxml.jackson.annotation; diff --git a/src/main/java/org/example/petshopdesktop/controllers/LoginController.java b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java index c007fdea..402dd9df 100644 --- a/src/main/java/org/example/petshopdesktop/controllers/LoginController.java +++ b/src/main/java/org/example/petshopdesktop/controllers/LoginController.java @@ -16,7 +16,7 @@ import org.example.petshopdesktop.api.dto.auth.LoginResponse; import org.example.petshopdesktop.api.dto.auth.UserInfoResponse; import org.example.petshopdesktop.auth.Role; import org.example.petshopdesktop.auth.UserSession; -import org.example.petshopdesktop.ui.SvgNodeLoader; +import org.example.petshopdesktop.ui.SvgWebViewFactory; import org.example.petshopdesktop.util.ActivityLogger; public class LoginController { @@ -36,7 +36,7 @@ public class LoginController { @FXML public void initialize() { lblError.setText(""); - logoContainer.getChildren().setAll(SvgNodeLoader.loadSquare("/org/example/petshopdesktop/images/leons-pet-store-badge-text.svg", 132)); + logoContainer.getChildren().setAll(SvgWebViewFactory.build("/org/example/petshopdesktop/images/leons-pet-store-badge-text.svg", 132)); } @FXML diff --git a/src/main/java/org/example/petshopdesktop/ui/SvgNodeLoader.java b/src/main/java/org/example/petshopdesktop/ui/SvgNodeLoader.java deleted file mode 100644 index 262aab23..00000000 --- a/src/main/java/org/example/petshopdesktop/ui/SvgNodeLoader.java +++ /dev/null @@ -1,201 +0,0 @@ -package org.example.petshopdesktop.ui; - -import javafx.scene.Group; -import javafx.scene.layout.Pane; -import javafx.scene.paint.Paint; -import javafx.scene.shape.Circle; -import javafx.scene.shape.FillRule; -import javafx.scene.shape.Rectangle; -import javafx.scene.shape.Shape; -import javafx.scene.shape.SVGPath; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -import javax.xml.parsers.DocumentBuilderFactory; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -public final class SvgNodeLoader { - private SvgNodeLoader() { - } - - public static Pane loadSquare(String resourcePath, double size) { - SvgDocument svg = readSvg(resourcePath); - double inset = size * 0.08; - double available = size - (inset * 2); - double scale = Math.min(available / svg.width(), available / svg.height()); - - Group graphic = svg.content(); - graphic.setScaleX(scale); - graphic.setScaleY(scale); - graphic.setLayoutX(inset); - graphic.setLayoutY(inset); - - Pane pane = new Pane(graphic); - pane.setMinSize(size, size); - pane.setPrefSize(size, size); - pane.setMaxSize(size, size); - pane.setClip(new Rectangle(size, size)); - return pane; - } - - private static SvgDocument readSvg(String resourcePath) { - try (Reader reader = requireResource(resourcePath)) { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(false); - Document document = factory.newDocumentBuilder().parse(new InputSource(reader)); - Element root = document.getDocumentElement(); - double[] viewBox = parseViewBox(root.getAttribute("viewBox")); - Map> styles = parseStyles(root); - Group content = new Group(); - appendChildren(root, content, styles); - content.setTranslateX(-viewBox[0]); - content.setTranslateY(-viewBox[1]); - return new SvgDocument(content, viewBox[2], viewBox[3]); - } catch (Exception e) { - throw new IllegalStateException("Unable to load SVG: " + resourcePath, e); - } - } - - private static InputStreamReader requireResource(String resourcePath) { - var stream = SvgNodeLoader.class.getResourceAsStream(resourcePath); - if (stream == null) { - throw new IllegalArgumentException("Resource not found: " + resourcePath); - } - return new InputStreamReader(stream, StandardCharsets.UTF_8); - } - - private static double[] parseViewBox(String viewBox) { - String[] parts = viewBox.trim().split("\\s+"); - if (parts.length != 4) { - throw new IllegalArgumentException("Invalid viewBox: " + viewBox); - } - return new double[] { - Double.parseDouble(parts[0]), - Double.parseDouble(parts[1]), - Double.parseDouble(parts[2]), - Double.parseDouble(parts[3]) - }; - } - - private static Map> parseStyles(Element root) { - Map> styles = new HashMap<>(); - NodeList styleNodes = root.getElementsByTagName("style"); - for (int i = 0; i < styleNodes.getLength(); i++) { - String css = styleNodes.item(i).getTextContent(); - for (String rule : css.split("}")) { - String trimmedRule = rule.trim(); - if (trimmedRule.isEmpty()) { - continue; - } - String[] selectorAndBody = trimmedRule.split("\\{", 2); - if (selectorAndBody.length != 2) { - continue; - } - String selector = selectorAndBody[0].trim(); - if (!selector.startsWith(".")) { - continue; - } - Map properties = new LinkedHashMap<>(); - for (String declaration : selectorAndBody[1].split(";")) { - String trimmedDeclaration = declaration.trim(); - if (trimmedDeclaration.isEmpty()) { - continue; - } - String[] keyValue = trimmedDeclaration.split(":", 2); - if (keyValue.length == 2) { - properties.put(keyValue[0].trim(), keyValue[1].trim()); - } - } - styles.put(selector.substring(1), properties); - } - } - return styles; - } - - private static void appendChildren(Element parent, Group target, Map> styles) { - NodeList childNodes = parent.getChildNodes(); - for (int i = 0; i < childNodes.getLength(); i++) { - org.w3c.dom.Node child = childNodes.item(i); - if (!(child instanceof Element element)) { - continue; - } - switch (element.getTagName()) { - case "g" -> { - Group group = new Group(); - appendChildren(element, group, styles); - target.getChildren().add(group); - } - case "circle" -> target.getChildren().add(buildCircle(element, styles)); - case "path" -> target.getChildren().add(buildPath(element, styles)); - default -> { - } - } - } - } - - private static Circle buildCircle(Element element, Map> styles) { - Circle circle = new Circle( - Double.parseDouble(element.getAttribute("cx")), - Double.parseDouble(element.getAttribute("cy")), - Double.parseDouble(element.getAttribute("r")) - ); - applyShapeStyle(circle, element, styles); - return circle; - } - - private static SVGPath buildPath(Element element, Map> styles) { - SVGPath path = new SVGPath(); - path.setContent(element.getAttribute("d")); - applyShapeStyle(path, element, styles); - return path; - } - - private static void applyShapeStyle(Shape shape, Element element, Map> styles) { - Map styleMap = new LinkedHashMap<>(); - String className = element.getAttribute("class"); - if (!className.isBlank()) { - for (String cssClass : className.trim().split("\\s+")) { - Map classStyles = styles.get(cssClass); - if (classStyles != null) { - styleMap.putAll(classStyles); - } - } - } - copyAttribute(element, styleMap, "fill"); - copyAttribute(element, styleMap, "stroke"); - copyAttribute(element, styleMap, "stroke-width"); - copyAttribute(element, styleMap, "fill-rule"); - - if (styleMap.containsKey("fill") && !"none".equalsIgnoreCase(styleMap.get("fill"))) { - shape.setFill(Paint.valueOf(styleMap.get("fill"))); - } else { - shape.setFill(null); - } - if (styleMap.containsKey("stroke") && !"none".equalsIgnoreCase(styleMap.get("stroke"))) { - shape.setStroke(Paint.valueOf(styleMap.get("stroke"))); - } - if (styleMap.containsKey("stroke-width")) { - shape.setStrokeWidth(Double.parseDouble(styleMap.get("stroke-width"))); - } - if (styleMap.containsKey("fill-rule") && shape instanceof SVGPath svgPath) { - svgPath.setFillRule("evenodd".equalsIgnoreCase(styleMap.get("fill-rule")) ? FillRule.EVEN_ODD : FillRule.NON_ZERO); - } - } - - private static void copyAttribute(Element element, Map styleMap, String attribute) { - String value = element.getAttribute(attribute); - if (!value.isBlank()) { - styleMap.put(attribute, value); - } - } - - private record SvgDocument(Group content, double width, double height) { - } -} diff --git a/src/main/java/org/example/petshopdesktop/ui/SvgWebViewFactory.java b/src/main/java/org/example/petshopdesktop/ui/SvgWebViewFactory.java new file mode 100644 index 00000000..5b3ed1dc --- /dev/null +++ b/src/main/java/org/example/petshopdesktop/ui/SvgWebViewFactory.java @@ -0,0 +1,48 @@ +package org.example.petshopdesktop.ui; + +import javafx.scene.layout.StackPane; +import javafx.scene.web.WebView; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public final class SvgWebViewFactory { + private SvgWebViewFactory() { + } + + public static StackPane build(String resourcePath, double size) { + WebView webView = new WebView(); + webView.setContextMenuEnabled(false); + webView.setPrefSize(size, size); + webView.setMinSize(size, size); + webView.setMaxSize(size, size); + webView.setZoom(1.0); + webView.setStyle("-fx-background-color: transparent;"); + + String svg = loadSvg(resourcePath) + .replaceFirst(""; + webView.getEngine().loadContent(html, "text/html"); + + StackPane container = new StackPane(webView); + container.setPrefSize(size, size); + container.setMinSize(size, size); + container.setMaxSize(size, size); + return container; + } + + private static String loadSvg(String resourcePath) { + try (InputStream stream = SvgWebViewFactory.class.getResourceAsStream(resourcePath)) { + if (stream == null) { + throw new IllegalArgumentException("Resource not found: " + resourcePath); + } + return new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException("Unable to read SVG: " + resourcePath, e); + } + } +}