Skip to content

Commit

Permalink
Add support for Ionicons (symbols) to <l:Icon> and <l:task> components (
Browse files Browse the repository at this point in the history
  • Loading branch information
janfaracik authored Feb 12, 2022
1 parent 731a297 commit 81072df
Show file tree
Hide file tree
Showing 61 changed files with 311 additions and 256 deletions.
2 changes: 1 addition & 1 deletion core/src/main/java/hudson/AboutJenkins.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
public class AboutJenkins extends ManagementLink {
@Override
public String getIconFileName() {
return "help.svg";
return "symbol-help-circle";
}

@Override
Expand Down
63 changes: 63 additions & 0 deletions core/src/main/java/hudson/Functions.java
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
import org.apache.commons.jexl.parser.ASTSizeFunction;
import org.apache.commons.jexl.util.Introspector;
import org.apache.commons.lang.StringUtils;
import org.jenkins.ui.icon.Icon;
import org.jenkins.ui.icon.IconSet;
import org.jvnet.tiger_types.Types;
import org.kohsuke.accmod.Restricted;
Expand Down Expand Up @@ -2290,6 +2291,68 @@ public static boolean isContextMenuVisible(Action a) {
}
}

@Restricted(NoExternalUse.class)
public static Icon tryGetIcon(String iconGuess) {
// Jenkins Symbols don't have metadata so return null
if (iconGuess == null || iconGuess.startsWith("symbol-")) {
return null;
}

StaplerRequest currentRequest = Stapler.getCurrentRequest();
currentRequest.getWebApp().getDispatchValidator().allowDispatch(currentRequest, Stapler.getCurrentResponse());
Icon iconMetadata = IconSet.icons.getIconByClassSpec(iconGuess);

if (iconMetadata == null) {
// Icon could be provided as a simple iconFileName e.g. "settings.png"
iconMetadata = IconSet.icons.getIconByClassSpec(IconSet.toNormalizedIconNameClass(iconGuess) + " icon-md");
}

if (iconMetadata == null) {
// Icon could be provided as an absolute iconFileName e.g. "/plugin/foo/abc.png"
iconMetadata = IconSet.icons.getIconByUrl(iconGuess);
}

return iconMetadata;
}

@Restricted(NoExternalUse.class)
public static String tryGetIconPath(String iconGuess, JellyContext context) {
if (iconGuess == null) {
return null;
}

if (iconGuess.startsWith("symbol-")) {
return iconGuess;
}

StaplerRequest currentRequest = Stapler.getCurrentRequest();
currentRequest.getWebApp().getDispatchValidator().allowDispatch(currentRequest, Stapler.getCurrentResponse());
String rootURL = currentRequest.getContextPath();
Icon iconMetadata = tryGetIcon(iconGuess);
String iconSource = null;

if (iconMetadata != null) {
iconSource = iconMetadata.getQualifiedUrl(context);
}

if (iconMetadata == null) {
if (!iconGuess.startsWith("/")) {
iconGuess = "/" + iconGuess;
}

iconSource = rootURL + (iconGuess.startsWith("/images/") || iconGuess.startsWith("/plugin/") ? getResourcePath() : "") + iconGuess;
}

if (iconMetadata != null && iconMetadata.getClassSpec() != null) {
String translatedIcon = IconSet.tryTranslateTangoIconToSymbol(iconMetadata.getClassSpec());
if (translatedIcon != null) {
return translatedIcon;
}
}

return iconSource;
}

@SuppressFBWarnings(value = "PREDICTABLE_RANDOM", justification = "True randomness isn't necessary for form item IDs")
@Restricted(NoExternalUse.class)
public static String generateItemId() {
Expand Down
8 changes: 6 additions & 2 deletions core/src/main/java/hudson/model/Action.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,26 @@
*/
public interface Action extends ModelObject {
/**
* Gets the file name of the icon.
* Gets the name of the icon.
*
* @return
* If the icon name is prefixed with "symbol-", a Jenkins Symbol
* will be used.
* <p>
* If just a file name (like "abc.gif") is returned, it will be
* interpreted as a file name inside {@code /images/24x24}.
* This is useful for using one of the stock images.
* <p>
* If an absolute file name that starts from '/' is returned (like
* "/plugin/foo/abc.gif'), then it will be interpreted as a path
* "/plugin/foo/abc.gif"), then it will be interpreted as a path
* from the context root of Jenkins. This is useful to pick up
* image files from a plugin.
* <p>
* Finally, return null to hide it from the task list. This is normally not very useful,
* but this can be used for actions that only contribute {@code floatBox.jelly}
* and no task list item. The other case where this is useful is
* to avoid showing links that require a privilege when the user is anonymous.
* @see <a href="https://www.jenkins.io/doc/developer/views/symbols/">Jenkins Symbols</a>
* @see Functions#isAnonymous()
* @see Functions#getIconFilePath(Action)
*/
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/hudson/model/ManageJenkinsAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class ManageJenkinsAction implements RootAction {
@Override
public String getIconFileName() {
if (Jenkins.get().hasAnyPermission(Jenkins.MANAGE, Jenkins.SYSTEM_READ))
return "gear.png";
return "symbol-settings";
else
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/jenkins/management/ConfigureLink.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class ConfigureLink extends ManagementLink {

@Override
public String getIconFileName() {
return "gear.svg";
return "symbol-settings";
}

@Override
Expand Down
22 changes: 22 additions & 0 deletions core/src/main/java/jenkins/model/ModelObjectWithContextMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ public ContextMenu add(String url, String icon, String text, boolean post, boole
return this;
}

/** @since TODO */
public ContextMenu add(String url, String icon, String iconXml, String text, boolean post, boolean requiresConfirmation) {
if (text != null && icon != null && url != null) {
MenuItem item = new MenuItem(url, icon, text);
item.iconXml = iconXml;
item.post = post;
item.requiresConfirmation = requiresConfirmation;
items.add(item);
}
return this;
}

/**
* Add a header row (no icon, no URL, rendered in header style).
*
Expand Down Expand Up @@ -268,6 +280,11 @@ class MenuItem {
@SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD", justification = "read by Stapler")
public String icon;

/**
* Optional icon XML, if set it's used instead of @icon for the menu item
*/
private String iconXml;

/**
* True to make a POST request rather than GET.
* @since 1.504
Expand Down Expand Up @@ -300,6 +317,11 @@ class MenuItem {
@SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD", justification = "read by Stapler")
public ContextMenu subMenu;

@Exported
public String getIconXml() {
return iconXml;
}

public MenuItem(String url, String icon, String displayName) {
withUrl(url).withIcon(icon).withDisplayName(displayName);
}
Expand Down
74 changes: 46 additions & 28 deletions core/src/main/java/org/jenkins/ui/icon/IconSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
import org.apache.commons.io.IOUtils;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
* An icon set.
Expand All @@ -45,7 +47,7 @@ public class IconSet {


public static final IconSet icons = new IconSet();
private static final Map<String, String> IONICONS = new ConcurrentHashMap<>();
private static final Map<String, String> SYMBOLS = new ConcurrentHashMap<>();

private Map<String, Icon> iconsByCSSSelector = new ConcurrentHashMap<>();
private Map<String, Icon> iconsByUrl = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -73,34 +75,40 @@ private static String prependTitleIfRequired(String icon, String title) {
return icon;
}

public static String getIonicon(String name, String title) {
if (IONICONS.containsKey(name)) {
String icon = IONICONS.get(name);
return prependTitleIfRequired(icon, title);
// for Jelly
@Restricted(NoExternalUse.class)
public static String getSymbol(String name, String title, String classes) {
if (SYMBOLS.containsKey(name)) {
String symbol = SYMBOLS.get(name);
symbol = symbol.replaceAll("(class=\")[^&]*?(\")", "$1$2");
symbol = symbol.replaceAll("<svg", "<svg class=\"" + classes + "\"");
return prependTitleIfRequired(symbol, title);
}

// Load icon if it exists
InputStream inputStream = IconSet.class.getResourceAsStream("/images/ionicons/" + name + ".svg");
String ionicon = null;
// Load symbol if it exists
InputStream inputStream = IconSet.class.getResourceAsStream("/images/symbols/" + name + ".svg");
String symbol = null;

try {
if (inputStream != null) {
ionicon = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
symbol = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
}
} catch (IOException e) {
// ignored
}
if (ionicon == null) {
ionicon = PLACEHOLDER_SVG;
if (symbol == null) {
symbol = PLACEHOLDER_SVG;
}

ionicon = ionicon.replaceAll("(<title>)[^&]*(</title>)", "$1$2");
ionicon = ionicon.replaceAll("<svg", "<svg aria-hidden=\"true\"");
ionicon = ionicon.replace("stroke:#000", "stroke:currentColor");
symbol = symbol.replaceAll("(<title>)[^&]*(</title>)", "$1$2");
symbol = symbol.replaceAll("(class=\")[^&]*?(\")", "$1$2");
symbol = symbol.replaceAll("<svg", "<svg aria-hidden=\"true\"");
symbol = symbol.replaceAll("<svg", "<svg class=\"" + classes + "\"");
symbol = symbol.replace("stroke:#000", "stroke:currentColor");

IONICONS.put(name, ionicon);
SYMBOLS.put(name, symbol);

return prependTitleIfRequired(ionicon, title);
return prependTitleIfRequired(symbol, title);
}

public IconSet addIcon(Icon icon) {
Expand Down Expand Up @@ -517,24 +525,34 @@ private static void initializeSVGs() {
images.add("warning");
images.add("document-properties");

Map<String, String> materialIcons = new HashMap<>();
materialIcons.put("help", "svg-sprite-action-symbol.svg#ic_help_24px");

for (Map.Entry<String, String> size : sizes.entrySet()) {
for (String image : images) {
icons.addIcon(new Icon("icon-" + image + " " + size.getKey(),
"svgs/" + image + ".svg", size.getValue()));
}
}
}

for (Map.Entry<String, String> imageEntry : materialIcons.entrySet()) {
icons.addIcon(new Icon(
"icon-" + imageEntry.getKey() + " " + size.getKey(),
"material-icons/" + imageEntry.getValue(),
size.getValue(),
IconFormat.EXTERNAL_SVG_SPRITE
)
);
}
/**
* This is a temporary function to replace Tango icons across Jenkins and plugins with
* appropriate Jenkins Symbols
*
* @param tangoIcon A tango icon in the format 'icon-* size-*', e.g. 'icon-gear icon-lg'
* @return a Jenkins Symbol (if one exists) otherwise null
*/
@Restricted(NoExternalUse.class)
public static String tryTranslateTangoIconToSymbol(String tangoIcon) {
if (tangoIcon != null) {
tangoIcon = tangoIcon.split(" ")[0];
}

Map<String, String> translations = new HashMap<>();
translations.put("icon-clock", "symbol-play");
translations.put("icon-edit-delete", "symbol-trash");
translations.put("icon-gear", "symbol-settings");
translations.put("icon-gear2", "symbol-settings");
translations.put("icon-plugin", "symbol-plugins");

return translations.getOrDefault(tangoIcon, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ THE SOFTWARE.
value="${request.getParameter('filter')}"
placeholder="${%Search}"/>
<div class="jenkins-search__icon">
<l:ionicon name="search-outline"/>
<l:icon src="symbol-search"/>
</div>
</div>
</l:app-bar>
Expand Down
6 changes: 3 additions & 3 deletions core/src/main/resources/hudson/PluginManager/installed.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ THE SOFTWARE.
value="${request.getParameter('filter')}"
placeholder="${filtered == 'true' ? '%Search' : '%Filter'}"/>
<div class="jenkins-search__icon">
<l:ionicon name="search-outline"/>
<l:icon src="symbol-search"/>
</div>
</div>
</l:app-bar>
Expand Down Expand Up @@ -180,7 +180,7 @@ THE SOFTWARE.
<form class="jenkins-buttons-row" method="post"
action="${rootURL}/updateCenter/plugin/${p.shortName}/downgrade">
<button class="jenkins-table__button jenkins-table__button--orange" tooltip="${%downgradeTo(p.backupVersion)}">
<l:ionicon name="downgrade-circle-outline"/>
<l:icon src="symbol-downgrade-circle"/>
<span class="jenkins-!-margin-left-1"
style="font-size: 0.75rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 50px;">
${p.backupVersion}
Expand Down Expand Up @@ -226,7 +226,7 @@ THE SOFTWARE.
</j:if>
<form class="jenkins-buttons-row" method="post" action="plugin/${p.shortName}/uninstall">
<button class="jenkins-table__button jenkins-table__button--red uninstall" tooltip="${%Uninstall} ${p.updateInfo.displayName?:p.displayName}">
<l:ionicon name="close-circle-outline"/>
<l:icon src="symbol-close-circle"/>
</button>
</form>
</j:otherwise>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ l.header()
l.side_panel {
l.tasks {
l.task(icon:"icon-up icon-md", href:rootURL+'/', title:_("Back to Dashboard"))
l.task(icon:"icon-gear icon-md", href:"${rootURL}/manage", title:_("Manage Jenkins"))
l.task(icon:"symbol-settings", href:"${rootURL}/manage", title:_("Manage Jenkins"))
if (!app.updateCenter.jobs.isEmpty()) {
l.task(icon:"icon-plugin icon-md", href:"${rootURL}/updateCenter/", title:_("Update Center"))
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/resources/hudson/PluginManager/table.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ THE SOFTWARE.
value="${request.getParameter('filter')}"
placeholder="${filtered == 'true' ? '%Search' : '%Filter'}"/>
<div class="jenkins-search__icon">
<l:ionicon name="search-outline"/>
<l:icon src="symbol-search"/>
</div>
</div>
</l:app-bar>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ THE SOFTWARE.
<l:task href=".." icon="icon-up icon-md" title="${%Back to Loggers}"/>
<l:task href="." icon="icon-notepad icon-md" title="${%Log records}"/>
<l:isAdmin>
<l:task href="configure" icon="icon-gear icon-md" title="${%Configure}"/>
<l:task href="configure" icon="symbol-settings" title="${%Configure}"/>
<l:task href="delete" icon="icon-edit-delete icon-md" title="${%Delete}"/>
</l:isAdmin>
</l:tasks>
</l:side-panel>
</j:jelly>
</j:jelly>
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ THE SOFTWARE.
<l:isAdmin>
<div class="jenkins-table__cell__button-wrapper">
<a href="${lr.searchUrl}/configure" class="jenkins-table__button">
<l:ionicon name="settings-outline" />
<l:icon src="symbol-settings" />
</a>
</div>
</l:isAdmin>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ THE SOFTWARE.
<l:side-panel>
<l:tasks>
<l:task href="${rootURL}/" icon="icon-up icon-md" title="${%Back to Dashboard}"/>
<l:task href="${rootURL}/manage" icon="icon-gear icon-md" title="${%Manage Jenkins}"/>
<l:task href="${rootURL}/manage" icon="symbol-settings" title="${%Manage Jenkins}"/>
<l:task href="." icon="icon-clipboard icon-md" title="${%Log Recorders}">
<l:isAdmin>
<l:task href="new" icon="icon-new-package icon-md" title="${%New Log Recorder}"/>
</l:isAdmin>
</l:task>
<l:task href="all" icon="icon-notepad icon-md" title="${%All Log Messages}"/>
<l:task href="levels" icon="icon-gear icon-md" title="${%Log Levels}"/>
<l:task href="levels" icon="symbol-settings" title="${%Log Levels}"/>
</l:tasks>
</l:side-panel>
</j:jelly>
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ THE SOFTWARE.
<l:task contextMenu="false" href="${buildUrl.baseUrl}/" icon="icon-search icon-md" title="${%Status}"/>
<l:task href="${buildUrl.baseUrl}/changes" icon="icon-notepad icon-md" title="${%Changes}"/>
<p:console-link/>
<l:task href="${buildUrl.baseUrl}/configure" icon="icon-notepad icon-md" title="${h.hasPermission(it,it.UPDATE)?'%Edit Build Information':'%View Build Information'}"/>
</j:jelly>
<l:task href="${buildUrl.baseUrl}/configure" icon="symbol-settings" title="${h.hasPermission(it,it.UPDATE)?'%Edit Build Information':'%View Build Information'}"/>
</j:jelly>
Loading

0 comments on commit 81072df

Please sign in to comment.