Skip to content

Commit

Permalink
fix ikonli details views bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
leewyatt committed Sep 26, 2023
1 parent 38e9a30 commit 3bc20c1
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 98 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package com.dlsc.jfxcentral2.components.gridview;

import com.dlsc.jfxcentral2.utils.FileUtil;
import com.dlsc.jfxcentral2.utils.IkonUtil;
import com.dlsc.jfxcentral2.utils.IkonliPackUtil;
import com.dlsc.jfxcentral2.utils.OSUtil;
import com.dlsc.jfxcentral2.utils.SVGPathExtractor;
import com.dlsc.jfxcentral2.utils.WebAPIUtil;
import com.jpro.webapi.WebAPI;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
Expand All @@ -23,7 +21,6 @@
import one.jpro.routing.CopyUtil;
import org.apache.batik.dom.GenericDOMImplementation;
import org.apache.batik.svggen.SVGGraphics2D;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.kordamp.ikonli.Ikon;
Expand All @@ -40,21 +37,24 @@
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;

public class IkonDetailView extends DetailView<Ikon> {
protected static File initDirectory;
private static final String CACHE_PATH = ".jfxcentral/svgcache";
private static final String TEMP_DIR_PREFIX = "tempDir_";
private static final String TEMP_DIR_SUFFIX = "_dir";
private static final String SVG_EXTENSION = ".svg";
private static final Logger LOGGER = LogManager.getLogger(IkonDetailView.class);
private final HBox detailContent = new HBox();
private final StackPane previewPane = new StackPane();

private enum SVGType {
FILL,
OUTLINE
}

private record IkonInfo(String iconLiteral, String cssCode, String javaCode, String unicode, String mavenInfo,
String gradleInfo) {
String gradleInfo, String path) {
}

private final IkonInfo ikonInfo;
Expand All @@ -74,14 +74,17 @@ public IkonDetailView(Ikon item) {
item.getClass().getSimpleName() + "." + fontIcon.getIconCode(),
"\\u" + Integer.toHexString(item.getCode()),
IkonliPackUtil.getInstance().getMavenDependency(item),
IkonliPackUtil.getInstance().getGradleDependency(item));
IkonliPackUtil.getInstance().getGradleDependency(item),
extractorPathFromIcon(item.getDescription()));

StackPane previewPane = new StackPane();
previewPane.getChildren().setAll(fontIcon);
previewPane.getStyleClass().add("ikon-preview-wrapper");
HBox.setHgrow(previewPane, Priority.ALWAYS);

Node infoNode = createInfoNode();
StackPane.setAlignment(infoNode, Pos.CENTER);
HBox detailContent = new HBox();
detailContent.getChildren().setAll(previewPane, infoNode);
detailContent.getStyleClass().add("detail-content");
getChildren().setAll(detailContent);
Expand All @@ -98,56 +101,45 @@ private FlowPane createInfoNode() {
addRow(flowPane, "Unicode:", ikonInfo.unicode());
addRow(flowPane, "Maven:", ikonInfo.mavenInfo());
addRow(flowPane, "Gradle :", ikonInfo.gradleInfo());

ComboBox<SVGType> pathComboBox = createSvgTypeComboBox();
addRow(flowPane, "Path:", IkonUtil.copy, pathComboBox, event -> copyPathEventHandler(event, pathComboBox));

ComboBox<SVGType> svgComboBox = createSvgTypeComboBox();
addRow(flowPane, "SVG:", IkonUtil.download, svgComboBox, event -> downloadSVGHandler(svgComboBox));

addRow(flowPane, "Path:", ikonInfo.path());
addDownloadSvgRow(flowPane);
return flowPane;
}

private void downloadSVGHandler(ComboBox<SVGType> svgComboBox) {
SVGType type = svgComboBox.getSelectionModel().getSelectedItem();
SVGGraphics2D svgGenerator = getSvgGraphics2D(type);
SVGGraphics2D g2d = getSvgGraphics2D(type);
if (WebAPI.isBrowser()) {
webDownloadFile(svgGenerator);
webDownloadFile(g2d, type);
} else {
desktopDownloadFile(svgGenerator);
desktopDownloadFile(g2d, type);
}
}

private void copyPathEventHandler(ActionEvent event, ComboBox<SVGType> pathComboBox) {
File tempFile;
try {
tempFile = File.createTempFile("tempFile_", ".svg");
} catch (IOException e) {
throw new RuntimeException(e);
}
try (FileWriter writer = new FileWriter(tempFile)) {
SVGType type = pathComboBox.getSelectionModel().getSelectedItem();
getSvgGraphics2D(type).stream(writer, true);
String paths = SVGPathExtractor.extractPaths(tempFile);
CopyUtil.setCopyOnClick((Button) event.getSource(), paths);
} catch (IOException e) {
LOGGER.error("Failed to write svg file: {}", tempFile, e);
throw new RuntimeException(e);
}
}
private String extractorPathFromIcon(String iconLiteral) {
SVGType type = SVGType.FILL;
SVGGraphics2D g2d = getSvgGraphics2D(type);

private ComboBox<SVGType> createSvgTypeComboBox() {
ComboBox<SVGType> comboBox = new ComboBox<>(FXCollections.observableArrayList(SVGType.FILL, SVGType.OUTLINE));
comboBox.getSelectionModel().select(0);
comboBox.setFocusTraversable(false);
return comboBox;
File cacheDir = getCacheDirectory();
File tempFile = createSvgTempFile(g2d, cacheDir, type, iconLiteral);

String paths = SVGPathExtractor.extractPaths(tempFile);

g2d.dispose();
FileUtil.clearDirectory(cacheDir);
return paths;
}

private SVGGraphics2D getSvgGraphics2D(SVGType svgType) {
IkonHandler handler = IkonResolver.getInstance().resolve(getData().getDescription());
Font font;
try {
font = Font.createFont(Font.TRUETYPE_FONT, handler.getFontResourceAsStream()).deriveFont(24f);
/*
* The createFont method does not close the provided InputStream.
* Therefore, we use a try-with-resources block
* to ensure the InputStream gets closed properly.
*/
try (InputStream inputStream = handler.getFontResourceAsStream()) {
font = Font.createFont(Font.TRUETYPE_FONT, inputStream).deriveFont(24f);
} catch (FontFormatException | IOException e) {
LOGGER.error("Failed to load font: {}", handler.getFontFamily(), e);
throw new RuntimeException(e);
Expand All @@ -159,71 +151,74 @@ private SVGGraphics2D getSvgGraphics2D(SVGType svgType) {

DOMImplementation domImpl = GenericDOMImplementation.getDOMImplementation();
Document document = domImpl.createDocument("http://www.w3.org/2000/svg", "svg", null);
SVGGraphics2D svgGenerator = new SVGGraphics2D(document);
SVGGraphics2D g2d = new SVGGraphics2D(document);
if (svgType == SVGType.FILL) {
svgGenerator.fill(glyphShape);
g2d.fill(glyphShape);
} else {
svgGenerator.draw(glyphShape);
g2d.draw(glyphShape);
}
return svgGenerator;
return g2d;
}

private void webDownloadFile(SVGGraphics2D svgGenerator) {
File cacheDir = new File(new File(System.getProperty("user.home")), ".jfxcentral/svgcache");
if (!cacheDir.exists()) {
boolean cacheDirCreated = cacheDir.mkdir();
if (!cacheDirCreated) {
LOGGER.error("Failed to create cache directory: {}", cacheDir);
throw new RuntimeException("Failed to create svg cache directory: " + cacheDir);
}
}

File tempDir = null;
private void webDownloadFile(SVGGraphics2D g2d, SVGType type) {
File cacheDir = getCacheDirectory();
File tempFile = createSvgTempFile(g2d, cacheDir, type, ikonInfo.iconLiteral());
g2d.dispose();
try {
tempDir = File.createTempFile("tempDir_", "_dir", cacheDir);
if (!tempDir.delete() || !tempDir.mkdir()) {
LOGGER.error("Failed to create temp directory: {}", tempDir);
throw new IOException("Failed to create a temporary directory for SVG");
}
} catch (IOException e) {
LOGGER.error("Failed to create svg temp directory: {}", tempDir, e);
/*
* On Windows, the temporary files used for the download function might be occupied by the browser, causing delayed deletions to fail.
* Hence, we use the `clearDirectory` method which skips failed file deletions and proceeds with deleting the others.
* Icons that fail to be deleted on this attempt will typically be cleaned up during the next icon download.
* This ensures that the cache directory always contains the minimum number of files.
*/
WebAPIUtil.getWebAPI(this).downloadURL(tempFile.toURI().toURL(), () -> FileUtil.clearDirectory(cacheDir));
} catch (MalformedURLException e) {
LOGGER.error("Failed to convert svg file to URL: {}", tempFile, e);
throw new RuntimeException(e);
}
}

private File getCacheDirectory() {
File cacheDir = new File(System.getProperty("user.home"), CACHE_PATH);
if (!cacheDir.exists() && !cacheDir.mkdir()) {
LOGGER.error("Failed to create cache directory: {}", cacheDir);
throw new RuntimeException("Failed to create svg cache directory: " + cacheDir);
}
return cacheDir;
}

private File createSvgTempFile(SVGGraphics2D g2d, File cacheDir, SVGType type, String iconLiteral) {
File tempDir = createTempDirectory(cacheDir);
File tempFile = new File(tempDir, iconLiteral + "-" + type.toString().toLowerCase() + SVG_EXTENSION);

File tempFile = new File(tempDir, ikonInfo.iconLiteral + ".svg");
try (FileWriter writer = new FileWriter(tempFile)) {
svgGenerator.stream(writer, true);
g2d.stream(writer, true);
} catch (IOException e) {
LOGGER.error("Failed to write svg file: {}", tempFile, e);
LOGGER.error("Failed to write svg to file: {}", tempFile, e);
throw new RuntimeException(e);
}
return tempFile;
}

private File createTempDirectory(File parentDir) {
File tempDir = null;
try {
File finalTempDir = tempDir;
WebAPIUtil.getWebAPI(this).downloadURL(tempFile.toURI().toURL(), () -> {
try {
if (OSUtil.isWindows()) {
//TODO On Windows systems, temporary folders cannot be deleted; I need to try something else
//FileUtils.deleteDirectory(finalTempDir);
} else {
// TODO macOS test passed, but under Linux, it has not been tested yet
FileUtils.deleteDirectory(finalTempDir);
}
} catch (Exception e) {
LOGGER.error("Failed to delete temp directory: {}", finalTempDir, e);
throw new RuntimeException(e);
}
});
} catch (MalformedURLException e) {
LOGGER.error("Failed to convert svg file to URL: {}", tempFile, e);
tempDir = File.createTempFile(TEMP_DIR_PREFIX, TEMP_DIR_SUFFIX, parentDir);
if (!tempDir.delete() || !tempDir.mkdir()) {
LOGGER.error("Failed to create temp directory: {}", tempDir);
throw new IOException("Failed to create a temporary directory for SVG.");
}
} catch (IOException e) {
LOGGER.error("Failed to create svg temp directory: {}", tempDir, e);
throw new RuntimeException(e);
}
return tempDir;
}

private void desktopDownloadFile(SVGGraphics2D svgGenerator) {
private void desktopDownloadFile(SVGGraphics2D g2d, SVGType type) {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save File");
fileChooser.setInitialFileName(ikonInfo.iconLiteral + ".svg");
fileChooser.setInitialFileName(ikonInfo.iconLiteral + "-" + type.toString().toLowerCase() + SVG_EXTENSION);
if (initDirectory != null) {
fileChooser.setInitialDirectory(initDirectory);
}
Expand All @@ -236,9 +231,11 @@ private void desktopDownloadFile(SVGGraphics2D svgGenerator) {
if (file != null) {
try (FileWriter fileWriter = new FileWriter(file)) {
initDirectory = file.getParentFile();
svgGenerator.stream(fileWriter, true);
g2d.stream(fileWriter, true);
} catch (IOException e) {
LOGGER.error("Failed to write the file: {}", file, e);
} finally {
g2d.dispose();
}
}
}
Expand Down Expand Up @@ -266,19 +263,21 @@ private void addRow(FlowPane flowPane, String title, String contentText) {
flowPane.getChildren().add(box);
}

private void addRow(FlowPane flowPane, String title, Ikon ikon, Node node, EventHandler<ActionEvent> eventHandler) {
Label titleLabel = new Label(title);
titleLabel.managedProperty().bind(titleLabel.visibleProperty());
private void addDownloadSvgRow(FlowPane flowPane) {
Label titleLabel = new Label("SVG:");
titleLabel.getStyleClass().addAll("title");

ComboBox<SVGType> svgTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(SVGType.FILL, SVGType.OUTLINE));
svgTypeComboBox.getSelectionModel().select(0);
svgTypeComboBox.setFocusTraversable(false);

Button button = new Button();
button.setFocusTraversable(false);
button.getStyleClass().addAll("fill-button", "copy-button", "runnable-button");
button.setGraphic(new FontIcon(ikon));
button.managedProperty().bind(button.visibleProperty());
button.setOnAction(eventHandler);
button.getStyleClass().addAll("fill-button", "copy-button");
button.setGraphic(new FontIcon(IkonUtil.download));
button.setOnAction(event -> downloadSVGHandler(svgTypeComboBox));

HBox box = new HBox(titleLabel, node, button);
HBox box = new HBox(titleLabel, svgTypeComboBox, button);
box.getStyleClass().add("row-box");
flowPane.getChildren().add(box);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.dlsc.jfxcentral2.utils;

import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;

import java.io.StringReader;
/**
* EmptyEntityResolver is a singleton implementation of the EntityResolver interface.
* Its main purpose is to prevent the XML parser from trying to access external DTDs or schemas
* when parsing an XML document. This can be particularly useful to avoid unnecessary network
* requests or potential blocking issues, such as getting HTTP 429 response codes.
*
* By providing an empty InputSource, we essentially tell the XML parser that there are no external
* entities to resolve, and it should continue parsing without trying to fetch them.
*
* Typical use case:
* When parsing SVG or any XML documents that reference external DTDs or schemas,
* you can use this resolver to speed up parsing and avoid potential network-related issues.
*
*/
public enum EmptyEntityResolver implements EntityResolver {
INSTANCE;

@Override
public InputSource resolveEntity(String publicId, String systemId) {
return new InputSource(new StringReader(""));
}
}
35 changes: 35 additions & 0 deletions components/src/main/java/com/dlsc/jfxcentral2/utils/FileUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.dlsc.jfxcentral2.utils;

import java.io.File;

public class FileUtil {

/**
* Deletes files within a directory while skipping files that are in use.
* Delete files as much as possible, ignoring occupied files
* @param directory the directory to clear
*/
public static void clearDirectory(File directory) {
// check directory
if (directory == null || !directory.exists() || !directory.isDirectory()) {
return;
}

File[] files = directory.listFiles();
if (files == null) {
return;
}

for (File file : files) {
if (file.isFile()) {
// Ignore file deletion results;
// Because the file may be in use and cannot be deleted, we just have to try to delete it.
file.delete();
} else if (file.isDirectory()) {
clearDirectory(file);
file.delete();
}
}
}

}
Loading

0 comments on commit 3bc20c1

Please sign in to comment.