From 0eee144d260834fd88e79d319f6a793c6200bbfe Mon Sep 17 00:00:00 2001 From: mkpaz Date: Wed, 8 Feb 2023 18:02:21 +0400 Subject: [PATCH] Support icon color and add icons page --- .../atlantafx/sampler/layout/Sidebar.java | 2 + .../sampler/page/general/IconBrowser.java | 128 ++++++++++++++++ .../sampler/page/general/IconsPage.java | 143 ++++++++++++++++++ .../styles/scss/components/_icon-browser.scss | 26 ++++ .../assets/styles/scss/components/_index.scss | 3 +- styles/src/general/_extras.scss | 47 +++++- 6 files changed, 347 insertions(+), 2 deletions(-) create mode 100644 sampler/src/main/java/atlantafx/sampler/page/general/IconBrowser.java create mode 100644 sampler/src/main/java/atlantafx/sampler/page/general/IconsPage.java create mode 100644 sampler/src/main/resources/assets/styles/scss/components/_icon-browser.scss diff --git a/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java b/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java index 8cb330c..08cc3ae 100644 --- a/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java +++ b/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java @@ -5,6 +5,7 @@ import atlantafx.base.controls.Spacer; import atlantafx.base.theme.Styles; import atlantafx.sampler.page.Page; import atlantafx.sampler.page.components.*; +import atlantafx.sampler.page.general.IconsPage; import atlantafx.sampler.page.general.ThemePage; import atlantafx.sampler.page.general.TypographyPage; import atlantafx.sampler.page.showcase.filemanager.FileManagerPage; @@ -170,6 +171,7 @@ class Sidebar extends StackPane { caption("GENERAL"), navLink(ThemePage.NAME, ThemePage.class), navLink(TypographyPage.NAME, TypographyPage.class), + navLink(IconsPage.NAME, IconsPage.class), caption("COMPONENTS"), navLink(OverviewPage.NAME, OverviewPage.class), navLink(InputGroupPage.NAME, InputGroupPage.class), diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/IconBrowser.java b/sampler/src/main/java/atlantafx/sampler/page/general/IconBrowser.java new file mode 100644 index 0000000..3572b70 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/general/IconBrowser.java @@ -0,0 +1,128 @@ +package atlantafx.sampler.page.general; + +import atlantafx.base.theme.Tweaks; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.scene.control.*; +import org.kordamp.ikonli.Ikon; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; + +import static atlantafx.base.theme.Styles.TEXT_SMALL; + +public class IconBrowser extends TableView> { + + static final int FILTER_LEN = 2; + + private final int colNum; + private final List icons; + private final SimpleStringProperty filter = new SimpleStringProperty(); + + public IconBrowser(int colNum, List icons) { + super(); + + if (colNum <= 0) { + throw new IllegalArgumentException("Column count must be greater than 0."); + } + + if (icons == null || icons.isEmpty()) { + throw new IllegalArgumentException("Icon list cannot be null or empty."); + } + + this.colNum = colNum; + this.icons = icons; + + filterProperty().addListener((obs, old, val) -> updateData(val)); + + initTable(); + updateData(null); + } + + public SimpleStringProperty filterProperty() { + return filter; + } + + private void initTable() { + for (int i = 0; i < colNum; i++) { + var col = new TableColumn, Ikon>("col" + i); + final int colIndex = i; + col.setCellValueFactory(cb -> { + var row = cb.getValue(); + var item = row.size() > colIndex ? row.get(colIndex) : null; + return new SimpleObjectProperty<>(item); + }); + col.setCellFactory(cb -> new FontIconCell()); + col.getStyleClass().add(Tweaks.ALIGN_CENTER); + getColumns().add(col); + } + + setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + getSelectionModel().setCellSelectionEnabled(true); + getStyleClass().add("icon-browser"); + } + + private void updateData(String filterString) { + var displayedIcons = filterString == null || filterString.isBlank() || filterString.length() < FILTER_LEN + ? icons + : icons.stream().filter(icon -> containsString(icon.getDescription(), filterString)).toList(); + + var data = partitionList(displayedIcons, colNum); + getItems().setAll(data); + } + + private Collection> partitionList(List list, int size) { + List> partitions = new ArrayList<>(); + if (list.size() == 0) { + return partitions; + } + + int length = list.size(); + int numOfPartitions = length / size + ((length % size == 0) ? 0 : 1); + + for (int i = 0; i < numOfPartitions; i++) { + int from = i * size; + int to = Math.min((i * size + size), length); + partitions.add(list.subList(from, to)); + } + return partitions; + } + + private boolean containsString(String s1, String s2) { + return s1.toLowerCase(Locale.ROOT).contains(s2.toLowerCase(Locale.ROOT)); + } + + /////////////////////////////////////////////////////////////////////////// + + public static class FontIconCell extends TableCell, Ikon> { + + private final Label root = new Label(); + private final FontIcon fontIcon = new FontIcon(); + + public FontIconCell() { + super(); + + root.setContentDisplay(ContentDisplay.TOP); + root.setGraphic(fontIcon); + root.setGraphicTextGap(10); + root.getStyleClass().addAll("icon-label", TEXT_SMALL); + } + + @Override + protected void updateItem(Ikon icon, boolean empty) { + super.updateItem(icon, empty); + + if (icon == null) { + setGraphic(null); + return; + } + + root.setText(icon.getDescription()); + fontIcon.setIconCode(icon); + setGraphic(root); + } + } +} \ No newline at end of file diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/IconsPage.java b/sampler/src/main/java/atlantafx/sampler/page/general/IconsPage.java new file mode 100644 index 0000000..056a89c --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/general/IconsPage.java @@ -0,0 +1,143 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.page.general; + +import atlantafx.base.controls.CustomTextField; +import atlantafx.base.theme.Styles; +import atlantafx.sampler.page.AbstractPage; +import atlantafx.sampler.page.SampleBlock; +import atlantafx.sampler.theme.CSSFragment; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; +import org.kordamp.ikonli.Ikon; +import org.kordamp.ikonli.javafx.FontIcon; +import org.kordamp.ikonli.javafx.StackedFontIcon; +import org.kordamp.ikonli.material2.Material2AL; +import org.kordamp.ikonli.material2.Material2MZ; +import org.kordamp.ikonli.material2.Material2OutlinedAL; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; + +import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP; +import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP; +import static atlantafx.sampler.util.Controls.hyperlink; + +public class IconsPage extends AbstractPage { + + public static final String NAME = "Icons"; + + @Override + public String getName() { return NAME; } + + public IconsPage() { + super(); + createView(); + } + + private void createView() { + var headerText = new TextFlow( + new Text("AtlantaFX supports "), + hyperlink("Ikonli", URI.create("https://kordamp.org/ikonli")), + new Text(" iconic fonts that can be used together with some JavaFX components.") + ); + + var browserText = new TextFlow( + new Text("There's a variety of icon packs. Sampler app uses "), + hyperlink("Material Icons", URI.create("https://kordamp.org/ikonli/cheat-sheet-material2.html")), + new Text(" you can preview below.") + ); + + var filterText = new CustomTextField(); + filterText.setLeft(new FontIcon(Material2MZ.SEARCH)); + filterText.setPrefWidth(300); + + var filterBox = new HBox(filterText); + filterBox.setAlignment(Pos.CENTER); + + var icons = new ArrayList(); + icons.addAll(Arrays.asList(Material2AL.values())); + icons.addAll(Arrays.asList(Material2MZ.values())); + + var iconBrowser = new IconBrowser(5, icons); + VBox.setVgrow(iconBrowser, Priority.ALWAYS); + iconBrowser.filterProperty().bind(filterText.textProperty()); + + setUserContent(new VBox( + PAGE_VGAP, + headerText, + expandingHBox(colorSample(), stackingSample()), + browserText, + filterBox, + iconBrowser + )); + } + + private SampleBlock colorSample() { + var accentIcon = new FontIcon(Material2MZ.THUMB_UP); + accentIcon.getStyleClass().add(Styles.ACCENT); + + var successIcon = new FontIcon(Material2MZ.THUMB_UP); + successIcon.getStyleClass().add(Styles.SUCCESS); + + var warningIcon = new FontIcon(Material2MZ.THUMB_UP); + warningIcon.getStyleClass().add(Styles.WARNING); + + var dangerIcon = new FontIcon(Material2MZ.THUMB_UP); + dangerIcon.getStyleClass().add(Styles.DANGER); + + var content = new VBox( + BLOCK_VGAP, + new Label("You can also use pseudo-classes to set icon color."), + new HBox(BLOCK_HGAP, accentIcon, successIcon, warningIcon, dangerIcon) + ); + + return new SampleBlock("Colors", content); + } + + private SampleBlock stackingSample() { + var outerIcon1 = new FontIcon(Material2OutlinedAL.BLOCK); + outerIcon1.getStyleClass().add("outer-icon"); + + var innerIcon1 = new FontIcon(Material2MZ.PHOTO_CAMERA); + innerIcon1.getStyleClass().add("inner-icon"); + + var stackIcon1 = new StackedFontIcon(); + stackIcon1.getChildren().addAll(innerIcon1, outerIcon1); + new CSSFragment(""" + .stacked-ikonli-font-icon > .outer-icon { + -fx-icon-size: 48px; + -fx-icon-color: -color-danger-emphasis; + } + .stacked-ikonli-font-icon > .inner-icon { + -fx-icon-size: 24px; + } + """).addTo(stackIcon1); + + var outerIcon2 = new FontIcon(Material2OutlinedAL.CHECK_BOX_OUTLINE_BLANK); + outerIcon2.getStyleClass().add("outer-icon"); + + var innerIcon2 = new FontIcon(Material2AL.LOCK); + innerIcon2.getStyleClass().add("inner-icon"); + + var stackIcon2 = new StackedFontIcon(); + stackIcon2.getChildren().addAll(outerIcon2, innerIcon2); + new CSSFragment(""" + .stacked-ikonli-font-icon > .outer-icon { + -fx-icon-size: 48px; + } + .stacked-ikonli-font-icon > .inner-icon { + -fx-icon-size: 24px; + } + """).addTo(stackIcon2); + + var content = new HBox(BLOCK_HGAP, stackIcon1, stackIcon2); + + return new SampleBlock("Stacking Icons", content); + } +} diff --git a/sampler/src/main/resources/assets/styles/scss/components/_icon-browser.scss b/sampler/src/main/resources/assets/styles/scss/components/_icon-browser.scss new file mode 100644 index 0000000..30ff2bb --- /dev/null +++ b/sampler/src/main/resources/assets/styles/scss/components/_icon-browser.scss @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +.icon-browser { + + -fx-border-color: transparent; + -color-cell-border: transparent; + + >.column-header-background { + -fx-max-height: 0; + -fx-pref-height: 0; + -fx-min-height: 0; + } + + .table-row-cell { + -fx-cell-size: 80px; + + .icon-label { + -fx-padding: 10px; + -fx-cursor: hand; + } + } + + .ikonli-font-icon { + -fx-icon-size: 36px; + } +} \ No newline at end of file diff --git a/sampler/src/main/resources/assets/styles/scss/components/_index.scss b/sampler/src/main/resources/assets/styles/scss/components/_index.scss index 2fb82dd..c20aa01 100644 --- a/sampler/src/main/resources/assets/styles/scss/components/_index.scss +++ b/sampler/src/main/resources/assets/styles/scss/components/_index.scss @@ -1,3 +1,4 @@ // SPDX-License-Identifier: MIT -@use "html-editor-fix" \ No newline at end of file +@use "html-editor-fix"; +@use "icon-browser"; \ No newline at end of file diff --git a/styles/src/general/_extras.scss b/styles/src/general/_extras.scss index 14cd5b0..f151195 100644 --- a/styles/src/general/_extras.scss +++ b/styles/src/general/_extras.scss @@ -3,7 +3,8 @@ @use "../settings/config" as cfg; @use "../settings/effects"; -@each $level, $radius in cfg.$elevation { +@each $level, +$radius in cfg.$elevation { .elevated-#{$level} { @include effects.shadow(cfg.$elevation-color, $radius); } @@ -12,3 +13,47 @@ .interactive:hover { @include effects.shadow(cfg.$elevation-color, cfg.$elevation-interactive); } + +/////////////////////////////////////////////////////////////////////////////// +// Ikonli // +/////////////////////////////////////////////////////////////////////////////// + +.ikonli-font-icon.accent { + -fx-fill: -color-accent-emphasis; + -fx-icon-color: -color-accent-emphasis; +} + +.ikonli-font-icon.success { + -fx-fill: -color-success-emphasis; + -fx-icon-color: -color-success-emphasis; +} + +.ikonli-font-icon.warning { + -fx-fill: -color-warning-emphasis; + -fx-icon-color: -color-warning-emphasis; +} + +.ikonli-font-icon.danger { + -fx-fill: -color-danger-emphasis; + -fx-icon-color: -color-danger-emphasis; +} + +.ikonli-font-icon:accent { + -fx-fill: -color-accent-emphasis; + -fx-icon-color: -color-accent-emphasis; +} + +.ikonli-font-icon:success { + -fx-fill: -color-success-emphasis; + -fx-icon-color: -color-success-emphasis; +} + +.ikonli-font-icon:warning { + -fx-fill: -color-warning-emphasis; + -fx-icon-color: -color-warning-emphasis; +} + +.ikonli-font-icon:danger { + -fx-fill: -color-danger-emphasis; + -fx-icon-color: -color-danger-emphasis; +} \ No newline at end of file