diff --git a/base/src/main/java/atlantafx/base/theme/Styles.java b/base/src/main/java/atlantafx/base/theme/Styles.java index 1046fcb..a9a93aa 100644 --- a/base/src/main/java/atlantafx/base/theme/Styles.java +++ b/base/src/main/java/atlantafx/base/theme/Styles.java @@ -2,6 +2,9 @@ package atlantafx.base.theme; +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.util.Base64; import java.util.Objects; import javafx.css.PseudoClass; import javafx.scene.Node; @@ -14,6 +17,8 @@ import javafx.scene.control.TabPane; @SuppressWarnings("unused") public final class Styles { + public static final String DATA_URI_PREFIX = "data:base64,"; + // Colors public static final String ACCENT = "accent"; @@ -92,6 +97,10 @@ public final class Styles { /** * Adds given style class to the node if it's not present, otherwise * removes it. + * + * @param node the target node + * @param styleClass the style class to be toggled + * @throws NullPointerException if node or style class is null */ public static void toggleStyleClass(Node node, String styleClass) { if (node == null) { @@ -102,7 +111,7 @@ public final class Styles { } int idx = node.getStyleClass().indexOf(styleClass); - if (idx > 0) { + if (idx >= 0) { node.getStyleClass().remove(idx); } else { node.getStyleClass().add(styleClass); @@ -113,6 +122,11 @@ public final class Styles { * Adds given style class to the node and removes the excluded classes. * This method is supposed to be used when only one from a set of classes * have to be present at once. + * + * @param node the target node + * @param styleClass the style class to be toggled + * @param excludes the style classes to be excluded + * @throws NullPointerException if node or styleClass is null */ public static void addStyleClass(Node node, String styleClass, String... excludes) { if (node == null) { @@ -125,13 +139,21 @@ public final class Styles { if (excludes != null && excludes.length > 0) { node.getStyleClass().removeAll(excludes); } - node.getStyleClass().add(styleClass); + + if (!node.getStyleClass().contains(styleClass)) { + node.getStyleClass().add(styleClass); + } } /** * Activates given pseudo-class to the node and deactivates the excluded pseudo-classes. * This method is supposed to be used when only one from a set of pseudo-classes * have to be present at once. + * + * @param node the node to activate the pseudo-class on + * @param pseudoClass the pseudo-class to be activated + * @param excludes the pseudo-classes to be deactivated + * @throws NullPointerException if node or pseudo-class is null */ public static void activatePseudoClass(Node node, PseudoClass pseudoClass, PseudoClass... excludes) { if (node == null) { @@ -149,17 +171,88 @@ public final class Styles { node.pseudoClassStateChanged(pseudoClass, true); } + /** + * Appends CSS style declaration to the specified node. + * There's no check for duplicates, so the CSS declarations with the same property + * name can be appended multiple times. + * + * @param node the node to append the new style declaration + * @param prop CSS property name + * @param value CSS property value + * @throws NullPointerException if node is null + */ public static void appendStyle(Node node, String prop, String value) { + if (node == null) { + throw new NullPointerException("Node cannot be null!"); + } + if (prop == null || prop.isBlank() || value == null || value.isBlank()) { - System.err.printf("Ignoring invalid style: property='%s', value='%s'%n", prop, value); + System.err.printf("Ignoring invalid style: property = '%s', value = '%s'%n", prop, value); return; } var style = Objects.requireNonNullElse(node.getStyle(), ""); - if (!style.endsWith(";")) { + if (!style.isEmpty() && !style.endsWith(";")) { style += ";"; } style = style + prop.trim() + ":" + value.trim() + ";"; node.setStyle(style); } + + /** + * Removes the specified CSS style declaration from the specified node. + * + * @param node the node to remove the style from + * @param prop the name of the style property to remove + * @throws NullPointerException if node is null + */ + public static void removeStyle(Node node, String prop) { + if (node == null) { + throw new NullPointerException("Node cannot be null!"); + } + + var currentStyle = node.getStyle(); + if (currentStyle == null || currentStyle.isBlank()) { + return; + } + + if (prop == null || prop.isBlank()) { + System.err.printf("Ignoring invalid property = '%s'%n", prop); + return; + } + + String[] stylePairs = currentStyle.split(";"); + var newStyle = new StringBuilder(); + + for (var s : stylePairs) { + String[] styleParts = s.split(":"); + if (!styleParts[0].trim().equals(prop)) { + newStyle.append(s); + newStyle.append(";"); + } + } + + node.setStyle(newStyle.toString()); + } + + /** + * Converts a CSS string to a Base64-encoded data URI. The resulting string is + * an inline data URI that can be applied to any node in the following manner: + * + *
{@code} + * var dataUri = Styles.toDataURI(); + * node.getStylesheets().add(dataUri); + * node.getStylesheets().contains(dataUri); + * node.getStylesheets().remove(dataUri); + *+ * + * @param css the CSS string to encode + * @return the resulting data URI string + */ + public static String toDataURI(String css) { + if (css == null) { + throw new NullPointerException("CSS string cannot be null!"); + } + return DATA_URI_PREFIX + new String(Base64.getEncoder().encode(css.getBytes(UTF_8)), UTF_8); + } } diff --git a/base/src/test/java/atlantafx/base/theme/StylesTest.java b/base/src/test/java/atlantafx/base/theme/StylesTest.java new file mode 100644 index 0000000..55f309d --- /dev/null +++ b/base/src/test/java/atlantafx/base/theme/StylesTest.java @@ -0,0 +1,332 @@ +package atlantafx.base.theme; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import java.util.Base64; +import javafx.css.PseudoClass; +import javafx.scene.layout.Region; +import org.junit.jupiter.api.Test; + +public class StylesTest { + + PseudoClass pcFirst = PseudoClass.getPseudoClass("first"); + PseudoClass pcSecond = PseudoClass.getPseudoClass("second"); + PseudoClass pcExcluded = PseudoClass.getPseudoClass("excluded"); + + @Test + @SuppressWarnings("DataFlowIssue") + public void testToggleStyleClassNPE() { + assertThatNullPointerException().isThrownBy( + () -> Styles.toggleStyleClass(new Region(), null) + ); + assertThatNullPointerException().isThrownBy( + () -> Styles.toggleStyleClass(null, "any") + ); + } + + @Test + public void testToggleStyleClassOn() { + var node = new Region(); + node.getStyleClass().add("first"); + assertThat(node.getStyleClass()).containsExactly("first"); + + Styles.toggleStyleClass(node, "second"); + assertThat(node.getStyleClass()).containsExactly("first", "second"); + } + + @Test + public void testToggleStyleClassMultipleOn() { + var node = new Region(); + node.getStyleClass().addAll("first", "second", "third"); + assertThat(node.getStyleClass()).containsExactly("first", "second", "third"); + + Styles.toggleStyleClass(node, "fourth"); + assertThat(node.getStyleClass()).containsExactly("first", "second", "third", "fourth"); + } + + @Test + public void testToggleStyleClassOff() { + var node = new Region(); + node.getStyleClass().add("sole"); + assertThat(node.getStyleClass()).containsExactly("sole"); + + Styles.toggleStyleClass(node, "sole"); + assertThat(node.getStyleClass()).isEmpty(); + } + + @Test + public void testToggleStyleClassMultipleOff() { + var node = new Region(); + node.getStyleClass().addAll("first", "second", "third"); + assertThat(node.getStyleClass()).containsExactly("first", "second", "third"); + + Styles.toggleStyleClass(node, "second"); + assertThat(node.getStyleClass()).containsExactly("first", "third"); + } + + /////////////////////////////////////////////////////////////////////////// + + @Test + @SuppressWarnings("DataFlowIssue") + public void testAddStyleClassClassNPE() { + assertThatNullPointerException().isThrownBy( + () -> Styles.addStyleClass(new Region(), null) + ); + assertThatNullPointerException().isThrownBy( + () -> Styles.addStyleClass(null, "any") + ); + } + + @Test + public void testAddStyleClassAdds() { + var node = new Region(); + node.getStyleClass().addAll("first"); + assertThat(node.getStyleClass()).containsExactly("first"); + + Styles.addStyleClass(node, "second"); + assertThat(node.getStyleClass()).containsExactly("first", "second"); + } + + @Test + public void testAddStyleClassExcludes() { + var node = new Region(); + node.getStyleClass().addAll("first", "excluded"); + assertThat(node.getStyleClass()).containsExactly("first", "excluded"); + + Styles.addStyleClass(node, "second", "excluded"); + assertThat(node.getStyleClass()).containsExactly("first", "second"); + } + + @Test + public void testAddStyleClassIgnoresDuplicate() { + var node = new Region(); + node.getStyleClass().addAll("first", "second", "excluded"); + assertThat(node.getStyleClass()).containsExactly("first", "second", "excluded"); + + Styles.addStyleClass(node, "second", "excluded"); + assertThat(node.getStyleClass()).containsExactly("first", "second"); + } + + /////////////////////////////////////////////////////////////////////////// + + @Test + @SuppressWarnings("DataFlowIssue") + public void testActivatePseudoClassNPE() { + assertThatNullPointerException().isThrownBy( + () -> Styles.activatePseudoClass(new Region(), null) + ); + assertThatNullPointerException().isThrownBy( + () -> Styles.activatePseudoClass(null, pcFirst) + ); + } + + @Test + public void testActivatePseudoClassActivates() { + var node = new Region(); + node.pseudoClassStateChanged(pcFirst, true); + assertThat(node.getPseudoClassStates()).containsExactly(pcFirst); + + Styles.activatePseudoClass(node, pcSecond); + assertThat(node.getPseudoClassStates()).containsExactly(pcFirst, pcSecond); + } + + @Test + public void testActivatePseudoClassExcludes() { + var node = new Region(); + node.pseudoClassStateChanged(pcFirst, true); + node.pseudoClassStateChanged(pcExcluded, true); + assertThat(node.getPseudoClassStates()).containsExactly(pcFirst, pcExcluded); + + Styles.activatePseudoClass(node, pcSecond, pcExcluded); + assertThat(node.getPseudoClassStates()).containsExactly(pcFirst, pcSecond); + } + + @Test + public void testActivatePseudoClassIgnoresDuplicate() { + var node = new Region(); + node.pseudoClassStateChanged(pcFirst, true); + node.pseudoClassStateChanged(pcSecond, true); + node.pseudoClassStateChanged(pcExcluded, true); + assertThat(node.getPseudoClassStates()).containsExactly(pcFirst, pcSecond, pcExcluded); + + Styles.activatePseudoClass(node, pcSecond, pcExcluded); + assertThat(node.getPseudoClassStates()).containsExactly(pcFirst, pcSecond); + } + + /////////////////////////////////////////////////////////////////////////// + + @Test + @SuppressWarnings("DataFlowIssue") + void testAppendStyleNullNode() { + assertThatNullPointerException().isThrownBy( + () -> Styles.appendStyle(null, "-fx-background-color", "red") + ); + } + + @Test + void testAppendStyleValid() { + var node = new Region(); + Styles.appendStyle(node, "-fx-background-color", "red"); + assertThat(node.getStyle()).isEqualTo("-fx-background-color:red;"); + } + + @Test + void testAppendStyleEmptyProperty() { + var node = new Region(); + Styles.appendStyle(node, "", "red"); + assertThat(node.getStyle()).isEmpty(); + } + + @Test + void testAppendStyleEmptyValue() { + var node = new Region(); + Styles.appendStyle(node, "-fx-background-color", ""); + assertThat(node.getStyle()).isEmpty(); + } + + @Test + void testAppendStyleNullProperty() { + var node = new Region(); + Styles.appendStyle(node, null, "red"); + assertThat(node.getStyle()).isEmpty(); + } + + @Test + void testAppendStyleNullValue() { + var node = new Region(); + Styles.appendStyle(node, "-fx-background-color", null); + assertThat(node.getStyle()).isEmpty(); + } + + @Test + void testAppendStyleMultipleProperties() { + var node = new Region(); + Styles.appendStyle(node, "-fx-background-color", "red"); + Styles.appendStyle(node, "-fx-text-fill", "white"); + assertThat(node.getStyle()).isEqualTo("-fx-background-color:red;-fx-text-fill:white;"); + } + + @Test + void testAppendStyleDuplicateProperty() { + var node = new Region(); + Styles.appendStyle(node, "-fx-background-color", "red"); + Styles.appendStyle(node, "-fx-background-color", "blue"); + // that's it, "append" appends, no check for duplicates + assertThat(node.getStyle()).isEqualTo("-fx-background-color:red;-fx-background-color:blue;"); + } + + /////////////////////////////////////////////////////////////////////////// + + @Test + void testRemoveStyleValidProperty() { + var node = new Region(); + node.setStyle("-fx-background-color:red;-fx-text-fill:white;"); + Styles.removeStyle(node, "-fx-background-color"); + assertThat(node.getStyle()) + .contains("-fx-text-fill:white;") + .doesNotContain("-fx-background-color:red;"); + } + + @Test + void testRemoveStyleNonexistentProperty() { + var node = new Region(); + node.setStyle("-fx-background-color:red;"); + Styles.removeStyle(node, "-fx-text-fill"); + assertThat(node.getStyle()).contains("-fx-background-color:red;"); + } + + @Test + void testRemoveStyleNullProperty() { + var node = new Region(); + node.setStyle("-fx-background-color:red;"); + Styles.removeStyle(node, null); + assertThat(node.getStyle()).contains("-fx-background-color:red;"); + } + + @Test + void testRemoveStyleEmptyProperty() { + var node = new Region(); + node.setStyle("-fx-background-color:red;"); + Styles.removeStyle(node, ""); + assertThat(node.getStyle()).contains("-fx-background-color:red;"); + } + + @Test + @SuppressWarnings("DataFlowIssue") + void testRemoveStyleNullNode() { + assertThatNullPointerException().isThrownBy( + () -> Styles.removeStyle(null, "-fx-background-color") + ); + } + + @Test + void testRemoveStyleFromEmptyNode() { + var node = new Region(); + Styles.removeStyle(node, "-fx-background-color"); + assertThat(node.getStyle()).isEmpty(); + } + + @Test + void testRemoveMultipleStyles() { + var node = new Region(); + node.setStyle("-fx-background-color:red;-fx-text-fill:white;"); + Styles.removeStyle(node, "-fx-background-color"); + Styles.removeStyle(node, "-fx-text-fill"); + assertThat(node.getStyle()).isEmpty(); + } + + /////////////////////////////////////////////////////////////////////////// + + @Test + void testToDataURIWithValidCSS() { + String css = "body { font-size: 16px; }"; + + String dataUri = Styles.toDataURI(css); + byte[] decodedBytes = Base64.getDecoder().decode(dataUri.substring(dataUri.indexOf(",") + 1)); + + assertThat(dataUri).startsWith(Styles.DATA_URI_PREFIX); + assertThat(new String(decodedBytes)).isEqualTo(css); + } + + @Test + void testToDataURIWithEmptyCSS() { + String css = ""; + + String dataUri = Styles.toDataURI(css); + byte[] decodedBytes = Base64.getDecoder().decode(dataUri.substring(dataUri.indexOf(",") + 1)); + + assertThat(dataUri).startsWith(Styles.DATA_URI_PREFIX); + assertThat(new String(decodedBytes)).isEmpty(); + } + + @Test + @SuppressWarnings("DataFlowIssue") + void testToDataURIWithNullCSS() { + assertThatNullPointerException().isThrownBy( + () -> Styles.toDataURI(null) + ); + } + + @Test + void testToDataURIWithWhitespaceCSS() { + String css = " "; + + String dataUri = Styles.toDataURI(css); + byte[] decodedBytes = Base64.getDecoder().decode(dataUri.substring(dataUri.indexOf(",") + 1)); + + assertThat(dataUri).startsWith(Styles.DATA_URI_PREFIX); + assertThat(new String(decodedBytes)).isEqualTo(css); + } + + @Test + void testToDataURIWithSpecialCharactersCSS() { + String css = "#id { background-image: url('https://example.com/bg.png'); }"; + + String dataUri = Styles.toDataURI(css); + byte[] decodedBytes = Base64.getDecoder().decode(dataUri.substring(dataUri.indexOf(",") + 1)); + + assertThat(dataUri).startsWith(Styles.DATA_URI_PREFIX); + assertThat(new String(decodedBytes)).isEqualTo(css); + } +} \ No newline at end of file diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ButtonPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ButtonPage.java index 7e72738..1a90ed1 100755 --- a/sampler/src/main/java/atlantafx/sampler/page/components/ButtonPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/components/ButtonPage.java @@ -7,7 +7,6 @@ import atlantafx.base.util.BBCodeParser; import atlantafx.sampler.page.ExampleBox; import atlantafx.sampler.page.OutlinePage; import atlantafx.sampler.page.Snippet; -import atlantafx.sampler.theme.CSSFragment; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.ContentDisplay; @@ -395,7 +394,7 @@ public final class ButtonPage extends OutlinePage { // -fx-font-size: 32px; // -fx-icon-size: 32px; // } - new CSSFragment(dataClass).addTo(iconBtn); + iconBtn.getStylesheets().add(Styles.toDataURI(dataClass)); //snippet_8:end var box = new HBox(HGAP_20, btn, iconBtn); diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/CalendarPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/CalendarPage.java index f5defd9..edf4dd3 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/components/CalendarPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/components/CalendarPage.java @@ -8,7 +8,6 @@ import atlantafx.base.util.BBCodeParser; import atlantafx.sampler.page.ExampleBox; import atlantafx.sampler.page.OutlinePage; import atlantafx.sampler.page.Snippet; -import atlantafx.sampler.theme.CSSFragment; import java.net.URI; import java.time.LocalDate; import java.time.LocalTime; @@ -148,7 +147,7 @@ public final class CalendarPage extends OutlinePage { } private ExampleBox styleExample() { - var style = """ + var dataClass = """ .date-picker-popup { -color-date-border: -color-accent-emphasis; -color-date-month-year-bg: -color-accent-emphasis; @@ -161,7 +160,7 @@ public final class CalendarPage extends OutlinePage { // -color-date-border: -color-accent-emphasis; // -color-date-month-year-bg: -color-accent-emphasis; // -color-date-month-year-fg: -color-fg-emphasis; - new CSSFragment(style).addTo(dp); + dp.getStylesheets().add(Styles.toDataURI(dataClass)); //snippet_4:end var box = new HBox(dp); diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/PopoverPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/PopoverPage.java index 7ef6473..b0d0832 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/components/PopoverPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/components/PopoverPage.java @@ -5,11 +5,11 @@ package atlantafx.sampler.page.components; import atlantafx.base.controls.InlineDatePicker; import atlantafx.base.controls.Popover; import atlantafx.base.controls.Popover.ArrowLocation; +import atlantafx.base.theme.Styles; import atlantafx.base.util.BBCodeParser; import atlantafx.sampler.page.ExampleBox; import atlantafx.sampler.page.OutlinePage; import atlantafx.sampler.page.Snippet; -import atlantafx.sampler.theme.CSSFragment; import java.net.URI; import java.time.LocalDate; import java.time.ZoneId; @@ -51,7 +51,7 @@ public final class PopoverPage extends OutlinePage { } private ExampleBox usageExample() { - var datePickerStyle = """ + var dataClass = """ .popover .date-picker-popup { -color-date-border: transparent; -color-date-bg: transparent; @@ -84,7 +84,7 @@ public final class PopoverPage extends OutlinePage { // -color-date-day-bg: transparent; // -color-date-month-year-bg: transparent; // -color-date-day-bg-hover: -color-bg-subtle; - new CSSFragment(datePickerStyle).addTo(datePicker); + datePicker.getStylesheets().add(Styles.toDataURI(dataClass)); var pop2 = new Popover(datePicker); pop2.setHeaderAlwaysVisible(false); diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/ProgressIndicatorPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/ProgressIndicatorPage.java index b258b5e..71d56f9 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/components/ProgressIndicatorPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/components/ProgressIndicatorPage.java @@ -8,7 +8,6 @@ import atlantafx.base.util.BBCodeParser; import atlantafx.sampler.page.ExampleBox; import atlantafx.sampler.page.OutlinePage; import atlantafx.sampler.page.Snippet; -import atlantafx.sampler.theme.CSSFragment; import javafx.beans.binding.Bindings; import javafx.concurrent.Task; import javafx.geometry.HPos; @@ -316,7 +315,7 @@ public final class ProgressIndicatorPage extends OutlinePage { // .example:danger .label { // -fx-text-fill: -color-fg-emphasis; // } - new CSSFragment(dataClass).addTo(content); + content.getStylesheets().add(Styles.toDataURI(dataClass)); bar.progressProperty().addListener((obs, old, val) -> { if (val == null) { diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/IconsPage.java b/sampler/src/main/java/atlantafx/sampler/page/general/IconsPage.java index 6147895..ebf48a2 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/general/IconsPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/general/IconsPage.java @@ -8,7 +8,6 @@ import atlantafx.base.util.BBCodeParser; import atlantafx.sampler.page.ExampleBox; import atlantafx.sampler.page.OutlinePage; import atlantafx.sampler.page.Snippet; -import atlantafx.sampler.theme.CSSFragment; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; @@ -80,7 +79,7 @@ public final class IconsPage extends OutlinePage { } private ExampleBox stackingExample() { - var style1 = """ + var dataClass1 = """ .stacked-ikonli-font-icon > .outer-icon { -fx-icon-size: 48px; -fx-icon-color: -color-danger-emphasis; @@ -90,7 +89,7 @@ public final class IconsPage extends OutlinePage { } """; - var style2 = """ + var dataClass2 = """ .stacked-ikonli-font-icon > .outer-icon { -fx-icon-size: 48px; } @@ -115,7 +114,7 @@ public final class IconsPage extends OutlinePage { // .stacked-ikonli-font-icon > .inner-icon { // -fx-icon-size: 24px; // } - new CSSFragment(style1).addTo(stackIcon1); + stackIcon1.getStylesheets().add(Styles.toDataURI(dataClass1)); var outerIcon2 = new FontIcon( Material2OutlinedAL.CHECK_BOX_OUTLINE_BLANK @@ -133,7 +132,7 @@ public final class IconsPage extends OutlinePage { // .stacked-ikonli-font-icon > .inner-icon { // -fx-icon-size: 24px; // } - new CSSFragment(style2).addTo(stackIcon2); + stackIcon2.getStylesheets().add(Styles.toDataURI(dataClass2)); //snippet_2:end var box = new HBox(HGAP_20, stackIcon1, stackIcon2); diff --git a/sampler/src/main/java/atlantafx/sampler/theme/CSSFragment.java b/sampler/src/main/java/atlantafx/sampler/theme/CSSFragment.java deleted file mode 100644 index 4bd0daa..0000000 --- a/sampler/src/main/java/atlantafx/sampler/theme/CSSFragment.java +++ /dev/null @@ -1,61 +0,0 @@ -/* SPDX-License-Identifier: MIT */ - -package atlantafx.sampler.theme; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.util.Base64; -import java.util.Objects; -import javafx.scene.layout.Region; - -public final class CSSFragment { - - private static final String DATA_URI_PREFIX = "data:base64,"; - - private final String css; - - public CSSFragment(String css) { - this.css = Objects.requireNonNull(css); - } - - public void addTo(Region region) { - Objects.requireNonNull(region); - region.getStylesheets().add(toDataURI()); - } - - public void removeFrom(Region region) { - Objects.requireNonNull(region); - region.getStylesheets().remove(toDataURI()); - } - - public boolean existsIn(Region region) { - Objects.requireNonNull(region); - return region.getStylesheets().contains(toDataURI()); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CSSFragment cssFragment = (CSSFragment) o; - return css.equals(cssFragment.css); - } - - @Override - public int hashCode() { - return Objects.hash(css); - } - - @Override - public String toString() { - return css; - } - - public String toDataURI() { - return DATA_URI_PREFIX + new String(Base64.getEncoder().encode(css.getBytes(UTF_8)), UTF_8); - } -}