Skip to content

Commit

Permalink
Merge pull request #1300 from lf-lang/icon-attribute
Browse files Browse the repository at this point in the history
Custom icons for reactors in diagrams
  • Loading branch information
lhstrh authored Aug 30, 2022
2 parents 52905e0 + c028a8c commit 71d832a
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 113 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,27 @@
***************/
package org.lflang.diagram.synthesis.util;

import com.google.inject.Inject;
import de.cau.cs.kieler.klighd.krendering.KContainerRendering;
import de.cau.cs.kieler.klighd.krendering.KGridPlacementData;
import de.cau.cs.kieler.klighd.krendering.KRectangle;
import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared;
import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions;
import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.URL;
import java.util.HashMap;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;

import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.ImageLoader;
import org.eclipse.xtext.xbase.lib.Exceptions;
import org.eclipse.xtext.xbase.lib.Extension;
import org.eclipse.xtext.xbase.lib.StringExtensions;
import org.lflang.ASTUtils;
import org.lflang.AttributeUtils;
import org.lflang.diagram.synthesis.AbstractSynthesisExtensions;
import org.lflang.lf.ReactorDecl;
import org.lflang.util.FileUtil;

import com.google.inject.Inject;

import de.cau.cs.kieler.klighd.krendering.Colors;
import de.cau.cs.kieler.klighd.krendering.KContainerRendering;
import de.cau.cs.kieler.klighd.krendering.KGridPlacementData;
import de.cau.cs.kieler.klighd.krendering.KRectangle;
import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared;
import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions;
import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions;

/**
* Utility class to handle icons for reactors in Lingua Franca diagrams.
Expand All @@ -63,127 +59,91 @@ public class ReactorIcons extends AbstractSynthesisExtensions {

private static final ImageLoader LOADER = new ImageLoader();

// memory-sensitive cache
private static final HashMap<URL, SoftReference<ImageData>> CACHE = new HashMap<>();
// Image cache during synthesis
private final HashMap<java.net.URI, ImageData> cache = new HashMap<>();

// Error message
private String error = null;

public void handleIcon(KContainerRendering rendering, ReactorDecl reactor, boolean collapsed) {
if (!collapsed) {
return;
}
URL iconLocation = locateIcon(reactor);
if (iconLocation != null) {
ImageData data = loadImage(iconLocation);
if (data != null) {
KRectangle figure = _kContainerRenderingExtensions.addRectangle(rendering);
_kRenderingExtensions.setInvisible(figure, true);
KGridPlacementData figurePlacement = _kRenderingExtensions.setGridPlacementData(figure, data.width, data.height);
_kRenderingExtensions.to(
_kRenderingExtensions.from(
figurePlacement,
_kRenderingExtensions.LEFT, 3, 0,
_kRenderingExtensions.TOP, 0, 0),
_kRenderingExtensions.RIGHT, 3, 0,
_kRenderingExtensions.BOTTOM, 3, 0);

KRectangle icon = _kContainerRenderingExtensions.addRectangle(figure);
_kRenderingExtensions.setInvisible(icon, true);
_kContainerRenderingExtensions.addImage(icon, data);
_kRenderingExtensions.setPointPlacementData(icon,
_kRenderingExtensions.createKPosition(
_kRenderingExtensions.LEFT, 0, 0.5f,
_kRenderingExtensions.TOP, 0, 0.5f),
_kRenderingExtensions.H_CENTRAL, _kRenderingExtensions.V_CENTRAL, 0,
0, data.width, data.height);
}

// Reset error
error = null;

// Get annotation
String iconPath = AttributeUtils.findAttributeByName(reactor, "icon");
if (iconPath == null) { // Fallback to old syntax (in comment)
iconPath = ASTUtils.findAnnotationInComments(reactor, "@icon");
}
}

private URL locateIcon(EObject eobj) {
URL location = null;
String iconPath = ASTUtils.findAnnotationInComments(eobj, "@icon");
if (!StringExtensions.isNullOrEmpty(iconPath)) {
// Check if path is URL
try {
return new URL(iconPath);
} catch (Exception e) {
// nothing
}
// Check if path exists as is
File path = new File(iconPath);
if (path.exists()) {
try {
return path.toURI().toURL();
} catch (Exception e) {
// nothing
}
}
// Check if path is relative to LF file
URI eURI = eobj.eResource() != null ? eobj.eResource().getURI() : null;
if (eURI != null) {
java.net.URI sourceURI = null;
try {
if (eURI.isFile()) {
sourceURI = new java.net.URI(eURI.toString());
sourceURI = new java.net.URI(sourceURI.getScheme(), null,
sourceURI.getPath().substring(0, sourceURI.getPath().lastIndexOf("/")), null);
} else if (eURI.isPlatformResource()) {
IResource iFile = ResourcesPlugin.getWorkspace().getRoot().findMember(eURI.toPlatformString(true));
sourceURI = iFile != null ? iFile.getRawLocation().toFile().getParentFile().toURI() : null;
} else if (eURI.isPlatformPlugin()) {
// TODO support loading from plugin bundles?
}
} catch (Exception e) {
// nothing
}
if (sourceURI != null) {
try {
location = sourceURI.resolve(path.toString()).toURL();
} catch (Exception e) {
// nothing
}
if (iconPath != null && !iconPath.isEmpty()) {
var iconLocation = FileUtil.locateFile(iconPath, reactor.eResource());
if (iconLocation == null) {
error = "Cannot find given icon file.";
} else {
ImageData data = loadImage(iconLocation);
if (data != null) {
KRectangle figure = _kContainerRenderingExtensions.addRectangle(rendering);
_kRenderingExtensions.setInvisible(figure, true);
KGridPlacementData figurePlacement = _kRenderingExtensions.setGridPlacementData(figure, data.width, data.height);
_kRenderingExtensions.to(
_kRenderingExtensions.from(
figurePlacement,
_kRenderingExtensions.LEFT, 3, 0,
_kRenderingExtensions.TOP, 0, 0),
_kRenderingExtensions.RIGHT, 3, 0,
_kRenderingExtensions.BOTTOM, 3, 0);

KRectangle icon = _kContainerRenderingExtensions.addRectangle(figure);
_kRenderingExtensions.setInvisible(icon, true);
_kContainerRenderingExtensions.addImage(icon, data);
_kRenderingExtensions.setPointPlacementData(icon,
_kRenderingExtensions.createKPosition(
_kRenderingExtensions.LEFT, 0, 0.5f,
_kRenderingExtensions.TOP, 0, 0.5f),
_kRenderingExtensions.H_CENTRAL, _kRenderingExtensions.V_CENTRAL, 0,
0, data.width, data.height);
}
if (error != null) {
var errorText = _kContainerRenderingExtensions.addText(rendering, "Icon not found!\n"+error);
_kRenderingExtensions.setForeground(errorText, Colors.RED);
_kRenderingExtensions.setFontBold(errorText, true);
_kRenderingExtensions.setSurroundingSpaceGrid(errorText, 8, 0);
}
}
// TODO more variants based on package and library system in LF
}
return location;
}

private ImageData loadImage(final URL url) {
private ImageData loadImage(final java.net.URI uri) {
try {
synchronized (CACHE) {
if (CACHE.containsKey(url)) {
ImageData img = CACHE.get(url).get();
if (img != null) {
return img;
} else {
CACHE.remove(url);
}
}
if (cache.containsKey(uri)) {
return cache.get(uri);
}
synchronized (LOADER) {
InputStream inStream = null;
try {
inStream = url.openStream();
// TODO check for memory leak !!!
inStream = uri.toURL().openStream();
ImageData[] data = LOADER.load(inStream);
if (data != null && data.length > 0) {
ImageData img = data[0];
synchronized (CACHE) {
CACHE.put(url, new SoftReference<ImageData>(img));
}
cache.put(uri, img);
return img;
} else {
error = "Could not load icon image.";
return null;
}
return null;
} finally {
if (inStream != null) {
inStream.close();
}
}
}
} catch (IOException ex) {
throw Exceptions.sneakyThrow(ex);
} catch (Exception ex) {
ex.printStackTrace();
error = "Could not load icon image.";
return null;
}
}

Expand Down
20 changes: 16 additions & 4 deletions org.lflang/src/org/lflang/AttributeUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@

/**
* A helper class for processing attributes in the AST.
*
* @author{Shaokai Lin <[email protected]>}
* @author{Clément Fournier, TU Dresden, INSA Rennes}
* @author{Alexander Schulz-Rosengarten <[email protected]>}
*/
public class AttributeUtils {

Expand Down Expand Up @@ -74,20 +76,30 @@ public static List<Attribute> getAttributes(EObject node) {
}

/**
* Return the value of the {@code @label} attribute if
* present, otherwise return null.
* Return the value of the attribute with the given name
* if present, otherwise return null.
*
* @throws IllegalArgumentException If the node cannot have attributes
*/
public static String findLabelAttribute(EObject node) {
public static String findAttributeByName(EObject node, String name) {
List<Attribute> attrs = getAttributes(node);
return attrs.stream()
.filter(it -> it.getAttrName().equals("label"))
.filter(it -> it.getAttrName().equalsIgnoreCase(name)) // case-insensitive search (more user-friendly)
.map(it -> it.getAttrParms().get(0).getValue().getStr())
.findFirst()
.orElse(null);
}

/**
* Return the value of the {@code @label} attribute if
* present, otherwise return null.
*
* @throws IllegalArgumentException If the node cannot have attributes
*/
public static String findLabelAttribute(EObject node) {
return findAttributeByName(node, "label");
}

/**
* Return true if the specified node is an Input and has an {@code @sparse}
* attribute.
Expand Down
53 changes: 53 additions & 0 deletions org.lflang/src/org/lflang/util/FileUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,59 @@ public static IPath toIPath(URI uri) throws IOException {
public static String toUnixString(Path path) {
return path.toString().replace('\\', '/');
}

/**
* Parse the string as file location and return it as URI.
* Supports URIs, plain file paths, and paths relative to a model.
*
* @param path the file location as string.
* @param resource the model resource this file should be resolved relatively. May be null.
* @return the (Java) URI or null if no file can be located.
*/
public static java.net.URI locateFile(String path, Resource resource) {
// Check if path is URL
try {
var uri = new java.net.URI(path);
if(uri.getScheme() != null) { // check if path was meant to be a URI
return uri;
}
} catch (Exception e) {
// nothing
}
// Check if path exists as it is
File file = new File(path);
if (file.exists()) {
try {
return file.toURI();
} catch (Exception e) {
// nothing
}
}
// Check if path is relative to LF file
if (resource != null) {
URI eURI = resource.getURI();
if (eURI != null) {
java.net.URI sourceURI = null;
try {
if (eURI.isFile()) {
sourceURI = new java.net.URI(eURI.toString());
sourceURI = new java.net.URI(sourceURI.getScheme(), null,
sourceURI.getPath().substring(0, sourceURI.getPath().lastIndexOf("/")), null);
} else if (eURI.isPlatformResource()) {
IResource iFile = ResourcesPlugin.getWorkspace().getRoot().findMember(eURI.toPlatformString(true));
sourceURI = iFile != null ? iFile.getRawLocation().toFile().getParentFile().toURI() : null;
}
if (sourceURI != null) {
return sourceURI.resolve(path.toString());
}
} catch (Exception e) {
// nothing
}
}
}
// fail
return null;
}

/**
* Recursively copies the contents of the given 'src'
Expand Down
4 changes: 4 additions & 0 deletions org.lflang/src/org/lflang/validation/AttributeSpec.java
Original file line number Diff line number Diff line change
Expand Up @@ -204,5 +204,9 @@ enum AttrParamType {
));
// @sparse
ATTRIBUTE_SPECS_BY_NAME.put("sparse", new AttributeSpec(null));
// @icon("value")
ATTRIBUTE_SPECS_BY_NAME.put("icon", new AttributeSpec(
List.of(new AttrParamSpec(AttributeSpec.VALUE_ATTR, AttrParamType.STRING, null))
));
}
}
33 changes: 33 additions & 0 deletions org.lflang/src/org/lflang/validation/LFValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import static org.lflang.ASTUtils.toDefinition;
import static org.lflang.ASTUtils.toOriginalText;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -57,6 +58,7 @@
import org.eclipse.xtext.validation.CheckType;
import org.eclipse.xtext.validation.ValidationMessageAcceptor;
import org.lflang.ASTUtils;
import org.lflang.AttributeUtils;
import org.lflang.ModelInfo;
import org.lflang.Target;
import org.lflang.TargetProperty;
Expand Down Expand Up @@ -1253,6 +1255,37 @@ public void checkWidthSpec(WidthSpec widthSpec) {
}
}
}

@Check(CheckType.FAST)
public void checkReactorIconAttribute(Reactor reactor) {
var attrs = AttributeUtils.getAttributes(reactor);
var iconAttr = attrs.stream()
.filter(it -> it.getAttrName().equalsIgnoreCase("icon"))
.findFirst()
.orElse(null);
if (iconAttr != null) {
var path = iconAttr.getAttrParms().get(0).getValue().getStr();

// Check file extension
var validExtensions = Set.of("bmp", "png", "gif", "ico", "jpeg");
var extensionStrart = path.lastIndexOf(".");
var extension = extensionStrart != -1 ? path.substring(extensionStrart + 1) : "";
if (!validExtensions.contains(extension.toLowerCase())) {
warning("File extension '" + extension + "' is not supported. Provide any of: " + String.join(", ", validExtensions),
iconAttr.getAttrParms().get(0), Literals.ATTR_PARM__VALUE);
return;
}

// Check file location
var iconLocation = FileUtil.locateFile(path, reactor.eResource());
if (iconLocation == null) {
warning("Cannot locate icon file.", iconAttr.getAttrParms().get(0), Literals.ATTR_PARM__VALUE);
}
if (("file".equals(iconLocation.getScheme()) || iconLocation.getScheme() == null) && !(new File(iconLocation.getPath()).exists())) {
warning("Icon does not exist.", iconAttr.getAttrParms().get(0), Literals.ATTR_PARM__VALUE);
}
}
}

@Check(CheckType.FAST)
public void checkInitialMode(Reactor reactor) {
Expand Down

0 comments on commit 71d832a

Please sign in to comment.