Improve utility methods in Styles class
- Added more methods to manipulate style properties. - Added unit tests.
This commit is contained in:
parent
c97142e9bf
commit
561bd78d23
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
package atlantafx.base.theme;
|
package atlantafx.base.theme;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import javafx.css.PseudoClass;
|
import javafx.css.PseudoClass;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
@ -14,6 +17,8 @@ import javafx.scene.control.TabPane;
|
|||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class Styles {
|
public final class Styles {
|
||||||
|
|
||||||
|
public static final String DATA_URI_PREFIX = "data:base64,";
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
|
|
||||||
public static final String ACCENT = "accent";
|
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
|
* Adds given style class to the node if it's not present, otherwise
|
||||||
* removes it.
|
* 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) {
|
public static void toggleStyleClass(Node node, String styleClass) {
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
@ -102,7 +111,7 @@ public final class Styles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int idx = node.getStyleClass().indexOf(styleClass);
|
int idx = node.getStyleClass().indexOf(styleClass);
|
||||||
if (idx > 0) {
|
if (idx >= 0) {
|
||||||
node.getStyleClass().remove(idx);
|
node.getStyleClass().remove(idx);
|
||||||
} else {
|
} else {
|
||||||
node.getStyleClass().add(styleClass);
|
node.getStyleClass().add(styleClass);
|
||||||
@ -113,6 +122,11 @@ public final class Styles {
|
|||||||
* Adds given style class to the node and removes the excluded classes.
|
* 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
|
* This method is supposed to be used when only one from a set of classes
|
||||||
* have to be present at once.
|
* 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) {
|
public static void addStyleClass(Node node, String styleClass, String... excludes) {
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
@ -125,13 +139,21 @@ public final class Styles {
|
|||||||
if (excludes != null && excludes.length > 0) {
|
if (excludes != null && excludes.length > 0) {
|
||||||
node.getStyleClass().removeAll(excludes);
|
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.
|
* 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
|
* This method is supposed to be used when only one from a set of pseudo-classes
|
||||||
* have to be present at once.
|
* 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) {
|
public static void activatePseudoClass(Node node, PseudoClass pseudoClass, PseudoClass... excludes) {
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
@ -149,17 +171,88 @@ public final class Styles {
|
|||||||
node.pseudoClassStateChanged(pseudoClass, true);
|
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) {
|
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()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var style = Objects.requireNonNullElse(node.getStyle(), "");
|
var style = Objects.requireNonNullElse(node.getStyle(), "");
|
||||||
if (!style.endsWith(";")) {
|
if (!style.isEmpty() && !style.endsWith(";")) {
|
||||||
style += ";";
|
style += ";";
|
||||||
}
|
}
|
||||||
style = style + prop.trim() + ":" + value.trim() + ";";
|
style = style + prop.trim() + ":" + value.trim() + ";";
|
||||||
node.setStyle(style);
|
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:
|
||||||
|
*
|
||||||
|
* <pre>{@code}
|
||||||
|
* var dataUri = Styles.toDataURI();
|
||||||
|
* node.getStylesheets().add(dataUri);
|
||||||
|
* node.getStylesheets().contains(dataUri);
|
||||||
|
* node.getStylesheets().remove(dataUri);
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
332
base/src/test/java/atlantafx/base/theme/StylesTest.java
Normal file
332
base/src/test/java/atlantafx/base/theme/StylesTest.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,6 @@ import atlantafx.base.util.BBCodeParser;
|
|||||||
import atlantafx.sampler.page.ExampleBox;
|
import atlantafx.sampler.page.ExampleBox;
|
||||||
import atlantafx.sampler.page.OutlinePage;
|
import atlantafx.sampler.page.OutlinePage;
|
||||||
import atlantafx.sampler.page.Snippet;
|
import atlantafx.sampler.page.Snippet;
|
||||||
import atlantafx.sampler.theme.CSSFragment;
|
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ContentDisplay;
|
import javafx.scene.control.ContentDisplay;
|
||||||
@ -395,7 +394,7 @@ public final class ButtonPage extends OutlinePage {
|
|||||||
// -fx-font-size: 32px;
|
// -fx-font-size: 32px;
|
||||||
// -fx-icon-size: 32px;
|
// -fx-icon-size: 32px;
|
||||||
// }
|
// }
|
||||||
new CSSFragment(dataClass).addTo(iconBtn);
|
iconBtn.getStylesheets().add(Styles.toDataURI(dataClass));
|
||||||
//snippet_8:end
|
//snippet_8:end
|
||||||
|
|
||||||
var box = new HBox(HGAP_20, btn, iconBtn);
|
var box = new HBox(HGAP_20, btn, iconBtn);
|
||||||
|
@ -8,7 +8,6 @@ import atlantafx.base.util.BBCodeParser;
|
|||||||
import atlantafx.sampler.page.ExampleBox;
|
import atlantafx.sampler.page.ExampleBox;
|
||||||
import atlantafx.sampler.page.OutlinePage;
|
import atlantafx.sampler.page.OutlinePage;
|
||||||
import atlantafx.sampler.page.Snippet;
|
import atlantafx.sampler.page.Snippet;
|
||||||
import atlantafx.sampler.theme.CSSFragment;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
@ -148,7 +147,7 @@ public final class CalendarPage extends OutlinePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ExampleBox styleExample() {
|
private ExampleBox styleExample() {
|
||||||
var style = """
|
var dataClass = """
|
||||||
.date-picker-popup {
|
.date-picker-popup {
|
||||||
-color-date-border: -color-accent-emphasis;
|
-color-date-border: -color-accent-emphasis;
|
||||||
-color-date-month-year-bg: -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-border: -color-accent-emphasis;
|
||||||
// -color-date-month-year-bg: -color-accent-emphasis;
|
// -color-date-month-year-bg: -color-accent-emphasis;
|
||||||
// -color-date-month-year-fg: -color-fg-emphasis;
|
// -color-date-month-year-fg: -color-fg-emphasis;
|
||||||
new CSSFragment(style).addTo(dp);
|
dp.getStylesheets().add(Styles.toDataURI(dataClass));
|
||||||
//snippet_4:end
|
//snippet_4:end
|
||||||
|
|
||||||
var box = new HBox(dp);
|
var box = new HBox(dp);
|
||||||
|
@ -5,11 +5,11 @@ package atlantafx.sampler.page.components;
|
|||||||
import atlantafx.base.controls.InlineDatePicker;
|
import atlantafx.base.controls.InlineDatePicker;
|
||||||
import atlantafx.base.controls.Popover;
|
import atlantafx.base.controls.Popover;
|
||||||
import atlantafx.base.controls.Popover.ArrowLocation;
|
import atlantafx.base.controls.Popover.ArrowLocation;
|
||||||
|
import atlantafx.base.theme.Styles;
|
||||||
import atlantafx.base.util.BBCodeParser;
|
import atlantafx.base.util.BBCodeParser;
|
||||||
import atlantafx.sampler.page.ExampleBox;
|
import atlantafx.sampler.page.ExampleBox;
|
||||||
import atlantafx.sampler.page.OutlinePage;
|
import atlantafx.sampler.page.OutlinePage;
|
||||||
import atlantafx.sampler.page.Snippet;
|
import atlantafx.sampler.page.Snippet;
|
||||||
import atlantafx.sampler.theme.CSSFragment;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
@ -51,7 +51,7 @@ public final class PopoverPage extends OutlinePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ExampleBox usageExample() {
|
private ExampleBox usageExample() {
|
||||||
var datePickerStyle = """
|
var dataClass = """
|
||||||
.popover .date-picker-popup {
|
.popover .date-picker-popup {
|
||||||
-color-date-border: transparent;
|
-color-date-border: transparent;
|
||||||
-color-date-bg: transparent;
|
-color-date-bg: transparent;
|
||||||
@ -84,7 +84,7 @@ public final class PopoverPage extends OutlinePage {
|
|||||||
// -color-date-day-bg: transparent;
|
// -color-date-day-bg: transparent;
|
||||||
// -color-date-month-year-bg: transparent;
|
// -color-date-month-year-bg: transparent;
|
||||||
// -color-date-day-bg-hover: -color-bg-subtle;
|
// -color-date-day-bg-hover: -color-bg-subtle;
|
||||||
new CSSFragment(datePickerStyle).addTo(datePicker);
|
datePicker.getStylesheets().add(Styles.toDataURI(dataClass));
|
||||||
|
|
||||||
var pop2 = new Popover(datePicker);
|
var pop2 = new Popover(datePicker);
|
||||||
pop2.setHeaderAlwaysVisible(false);
|
pop2.setHeaderAlwaysVisible(false);
|
||||||
|
@ -8,7 +8,6 @@ import atlantafx.base.util.BBCodeParser;
|
|||||||
import atlantafx.sampler.page.ExampleBox;
|
import atlantafx.sampler.page.ExampleBox;
|
||||||
import atlantafx.sampler.page.OutlinePage;
|
import atlantafx.sampler.page.OutlinePage;
|
||||||
import atlantafx.sampler.page.Snippet;
|
import atlantafx.sampler.page.Snippet;
|
||||||
import atlantafx.sampler.theme.CSSFragment;
|
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.geometry.HPos;
|
import javafx.geometry.HPos;
|
||||||
@ -316,7 +315,7 @@ public final class ProgressIndicatorPage extends OutlinePage {
|
|||||||
// .example:danger .label {
|
// .example:danger .label {
|
||||||
// -fx-text-fill: -color-fg-emphasis;
|
// -fx-text-fill: -color-fg-emphasis;
|
||||||
// }
|
// }
|
||||||
new CSSFragment(dataClass).addTo(content);
|
content.getStylesheets().add(Styles.toDataURI(dataClass));
|
||||||
|
|
||||||
bar.progressProperty().addListener((obs, old, val) -> {
|
bar.progressProperty().addListener((obs, old, val) -> {
|
||||||
if (val == null) {
|
if (val == null) {
|
||||||
|
@ -8,7 +8,6 @@ import atlantafx.base.util.BBCodeParser;
|
|||||||
import atlantafx.sampler.page.ExampleBox;
|
import atlantafx.sampler.page.ExampleBox;
|
||||||
import atlantafx.sampler.page.OutlinePage;
|
import atlantafx.sampler.page.OutlinePage;
|
||||||
import atlantafx.sampler.page.Snippet;
|
import atlantafx.sampler.page.Snippet;
|
||||||
import atlantafx.sampler.theme.CSSFragment;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -80,7 +79,7 @@ public final class IconsPage extends OutlinePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ExampleBox stackingExample() {
|
private ExampleBox stackingExample() {
|
||||||
var style1 = """
|
var dataClass1 = """
|
||||||
.stacked-ikonli-font-icon > .outer-icon {
|
.stacked-ikonli-font-icon > .outer-icon {
|
||||||
-fx-icon-size: 48px;
|
-fx-icon-size: 48px;
|
||||||
-fx-icon-color: -color-danger-emphasis;
|
-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 {
|
.stacked-ikonli-font-icon > .outer-icon {
|
||||||
-fx-icon-size: 48px;
|
-fx-icon-size: 48px;
|
||||||
}
|
}
|
||||||
@ -115,7 +114,7 @@ public final class IconsPage extends OutlinePage {
|
|||||||
// .stacked-ikonli-font-icon > .inner-icon {
|
// .stacked-ikonli-font-icon > .inner-icon {
|
||||||
// -fx-icon-size: 24px;
|
// -fx-icon-size: 24px;
|
||||||
// }
|
// }
|
||||||
new CSSFragment(style1).addTo(stackIcon1);
|
stackIcon1.getStylesheets().add(Styles.toDataURI(dataClass1));
|
||||||
|
|
||||||
var outerIcon2 = new FontIcon(
|
var outerIcon2 = new FontIcon(
|
||||||
Material2OutlinedAL.CHECK_BOX_OUTLINE_BLANK
|
Material2OutlinedAL.CHECK_BOX_OUTLINE_BLANK
|
||||||
@ -133,7 +132,7 @@ public final class IconsPage extends OutlinePage {
|
|||||||
// .stacked-ikonli-font-icon > .inner-icon {
|
// .stacked-ikonli-font-icon > .inner-icon {
|
||||||
// -fx-icon-size: 24px;
|
// -fx-icon-size: 24px;
|
||||||
// }
|
// }
|
||||||
new CSSFragment(style2).addTo(stackIcon2);
|
stackIcon2.getStylesheets().add(Styles.toDataURI(dataClass2));
|
||||||
//snippet_2:end
|
//snippet_2:end
|
||||||
|
|
||||||
var box = new HBox(HGAP_20, stackIcon1, stackIcon2);
|
var box = new HBox(HGAP_20, stackIcon1, stackIcon2);
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user