forked from dorkbox/SystemTray
Moved css parsing to it's own class, code polish, method refactoring for
legibilityty
This commit is contained in:
parent
803cea80b7
commit
951a600f0b
@ -1,10 +1,14 @@
|
||||
package dorkbox.systemTray.jna.linux;
|
||||
|
||||
import static dorkbox.systemTray.util.CssParser.CssNode;
|
||||
import static dorkbox.systemTray.util.CssParser.getAttributeFromSections;
|
||||
import static dorkbox.systemTray.util.CssParser.getSections;
|
||||
import static dorkbox.systemTray.util.CssParser.injectAdditionalCss;
|
||||
import static dorkbox.systemTray.util.CssParser.removeComments;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@ -21,6 +25,8 @@ import dorkbox.util.FileUtil;
|
||||
* as the text.
|
||||
* <p>
|
||||
* Additionally, CUSTOM, user theme modifications in ~/.gtkrc-2.0 (for example), will be ignored.
|
||||
*
|
||||
* Also note: not all themes have CSS or Theme files!!!
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public
|
||||
@ -29,11 +35,96 @@ class GtkTheme {
|
||||
private static final boolean DEBUG_SHOW_CSS = false;
|
||||
private static final boolean DEBUG_VERBOSE = false;
|
||||
|
||||
// size of GTK checkbox
|
||||
//
|
||||
/*
|
||||
// CSS nodes that we care about, in oder of preference from left to right.
|
||||
private static final
|
||||
String[] cssNodes = new String[] {"GtkPopover", "unity-panel", ".unity-panel", "gnome-panel-menu-bar", ".gnome-panel-menu-bar",
|
||||
"PanelMenuBar", ".menuitem", ".entry", "*"};
|
||||
|
||||
/**
|
||||
* Gets the text height of menu items (in the system tray menu), as specified by CSS.
|
||||
* NOTE: not all themes have CSS
|
||||
*/
|
||||
public static
|
||||
int getTextHeight() {
|
||||
String css = getCss();
|
||||
if (css != null) {
|
||||
System.err.println(css);
|
||||
// collect a list of all of the sections that have what we are interested in.
|
||||
List<CssNode> sections = getSections(css, cssNodes, null);
|
||||
String size = getAttributeFromSections(sections, cssNodes, "MenuItem-indicator-size", false);
|
||||
// CheckButton-indicator-size
|
||||
int i = stripNonDigits(size);
|
||||
if (i != 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// sane default
|
||||
return 14;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text padding of menu items (in the system tray menu), as specified by CSS. This is the padding value for all sides!
|
||||
* NOTE: not all themes have CSS
|
||||
*/
|
||||
public static
|
||||
int getTextPadding() {
|
||||
/*
|
||||
Maybe he best way is to get the element size, then subtract the text size.
|
||||
|
||||
The margin properties set the size of the white space outside the border.
|
||||
|
||||
we care about top and bottom padding
|
||||
|
||||
padding:10px 5px 15px 20px;
|
||||
top padding is 10px
|
||||
right padding is 5px
|
||||
bottom padding is 15px
|
||||
left padding is 20px
|
||||
|
||||
padding:10px 5px 15px;
|
||||
top padding is 10px
|
||||
right and left padding are 5px
|
||||
bottom padding is 15px
|
||||
|
||||
padding:10px 5px;
|
||||
top and bottom padding are 10px
|
||||
right and left padding are 5px
|
||||
|
||||
padding:10px;
|
||||
all four paddings are 10px
|
||||
|
||||
|
||||
GtkColorButton.button {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
|
||||
GtkPopover {
|
||||
margin: 10px;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
border-color: shade(@menu_bg_color, 0.8);
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
background-clip: border-box;
|
||||
background-image: none;
|
||||
background-color: @menu_bg_color;
|
||||
color: @menu_fg_color;
|
||||
box-shadow: 0 2px 3px alpha(black, 0.5);
|
||||
}
|
||||
|
||||
.menubar.menuitem,
|
||||
.menubar .menuitem {
|
||||
padding: 3px 8px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
color: @menubar_fg_color;
|
||||
}
|
||||
|
||||
-GtkCheckMenuItem-indicator-size: 14;
|
||||
|
||||
.entry {
|
||||
padding: 3px;
|
||||
@ -53,59 +144,67 @@ class GtkTheme {
|
||||
color: @theme_text_color;
|
||||
}
|
||||
|
||||
.button {
|
||||
-GtkWidget-focus-padding: 1;
|
||||
-GtkWidget-focus-line-width: 0;
|
||||
|
||||
.menuitem .entry {
|
||||
border-color: shade(@menu_bg_color, 0.7);
|
||||
background-color: @menu_bg_color;
|
||||
background-image: none;
|
||||
color: @menu_fg_color;
|
||||
}
|
||||
|
||||
GtkPopover {
|
||||
margin: 10px;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
border-color: shade(@menu_bg_color, 0.8);
|
||||
padding: 2px 4px;
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
border-style: solid;
|
||||
background-clip: border-box;
|
||||
background-image: none;
|
||||
background-color: @menu_bg_color;
|
||||
color: @menu_fg_color;
|
||||
box-shadow: 0 2px 3px alpha(black, 0.5);
|
||||
border-top-color: shade(@theme_bg_color, 0.8);
|
||||
border-right-color: shade(@theme_bg_color, 0.72);
|
||||
border-left-color: shade(@theme_bg_color, 0.72);
|
||||
border-bottom-color: shade(@theme_bg_color, 0.7);
|
||||
background-image: linear-gradient(to bottom,
|
||||
shade(shade(@theme_bg_color, 1.02), 1.05),
|
||||
shade(shade(@theme_bg_color, 1.02), 0.97)
|
||||
);
|
||||
|
||||
color: @theme_fg_color;
|
||||
}
|
||||
|
||||
GtkPopover .entry {
|
||||
border-color: mix(@menu_bg_color, @menu_fg_color, 0.12);
|
||||
background-color: @menu_bg_color;
|
||||
background-image: none;
|
||||
color: @menu_fg_color;
|
||||
}
|
||||
|
||||
*/
|
||||
public static
|
||||
int getTextHeight() {
|
||||
*/
|
||||
|
||||
|
||||
String css = getCss();
|
||||
if (css != null) {
|
||||
// collect a list of all of the sections that have what we are interested in.
|
||||
List<CssNode> sections = getSections(css, cssNodes, null);
|
||||
String padding = getAttributeFromSections(sections, cssNodes, "padding", true);
|
||||
String border = getAttributeFromSections(sections, cssNodes, "border-width", true);
|
||||
|
||||
return stripNonDigits(padding) + stripNonDigits(border);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return 9;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the system tray indicator size as specified by CSS.
|
||||
*/
|
||||
public static
|
||||
int getTextPadding() {
|
||||
return 9;
|
||||
int getIndicatorSize() {
|
||||
String css = getCss();
|
||||
if (css != null) {
|
||||
String[] cssNodes = new String[] {"GdMainIconView", ".content-view"};
|
||||
|
||||
// collect a list of all of the sections that have what we are interested in.
|
||||
List<CssNode> sections = getSections(css, cssNodes, null);
|
||||
String indicatorSize = getAttributeFromSections(sections, cssNodes, "-GdMainIconView-icon-size", true);
|
||||
|
||||
int i = stripNonDigits(indicatorSize);
|
||||
if (i != 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// sane default
|
||||
return 40;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @return the widget color of text for the current theme, or black. It is important that this is called AFTER GTK has been initialized.
|
||||
*/
|
||||
@ -274,62 +373,17 @@ GtkPopover .entry {
|
||||
System.err.println(css);
|
||||
}
|
||||
|
||||
// in order from left to right, try to get the color value
|
||||
String[] nodes = new String[] {"GtkPopover", "unity-panel", "gnome-panel-menu-bar", "PanelMenuBar", ".menuitem", ".entry"};
|
||||
|
||||
|
||||
// collect a list of all of the sections that have what we are interested in.
|
||||
List<CssNode> sections = getSections(css, nodes);
|
||||
|
||||
//
|
||||
String colorString = getAttributeFromSections(sections, nodes, "color");
|
||||
List<CssNode> sections = getSections(css, cssNodes, null);
|
||||
String colorString = getAttributeFromSections(sections, cssNodes, "color", true);
|
||||
|
||||
// hopefully we found it.
|
||||
if (colorString != null) {
|
||||
if (colorString.startsWith("@")) {
|
||||
// it's a color definition
|
||||
colorString = colorString.substring(1);
|
||||
String colorSubString = getColorDefinition(css, colorString.substring(1));
|
||||
|
||||
// have to setup the "define color" section
|
||||
String colorDefine = "@define-color";
|
||||
int start = css.indexOf(colorDefine);
|
||||
int end = css.lastIndexOf(colorDefine);
|
||||
end = css.lastIndexOf(";", end) + 1; // include the ;
|
||||
String colorDefines = css.substring(start, end);
|
||||
|
||||
if (DEBUG_VERBOSE) {
|
||||
System.err.println("+++++++++++++++++++++++");
|
||||
System.err.println(colorDefines);
|
||||
System.err.println("+++++++++++++++++++++++");
|
||||
}
|
||||
|
||||
// since it's a color definition, it will start a very specific way.
|
||||
String newColorString = colorDefine + " " + colorString;
|
||||
|
||||
int i = 0;
|
||||
while (i != -1) {
|
||||
i = colorDefines.indexOf(newColorString);
|
||||
|
||||
if (i >= 0) {
|
||||
try {
|
||||
int startIndex = i + newColorString.length();
|
||||
int endIndex = colorDefines.indexOf(";", i);
|
||||
|
||||
String colorSubString = colorDefines.substring(startIndex, endIndex)
|
||||
.trim();
|
||||
|
||||
if (colorSubString.startsWith("@")) {
|
||||
// have to recursively get the defined color
|
||||
newColorString = colorDefine + " " + colorSubString.substring(1);
|
||||
i = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
return parseColor(colorSubString);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return parseColor(colorSubString);
|
||||
}
|
||||
else {
|
||||
return parseColor(colorString);
|
||||
@ -340,6 +394,52 @@ GtkPopover .entry {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static
|
||||
String getColorDefinition(final String css, final String colorString) {
|
||||
// have to setup the "define color" section
|
||||
String colorDefine = "@define-color";
|
||||
int start = css.indexOf(colorDefine);
|
||||
int end = css.lastIndexOf(colorDefine);
|
||||
end = css.lastIndexOf(";", end) + 1; // include the ;
|
||||
String colorDefines = css.substring(start, end);
|
||||
|
||||
if (DEBUG_VERBOSE) {
|
||||
System.err.println("+++++++++++++++++++++++");
|
||||
System.err.println(colorDefines);
|
||||
System.err.println("+++++++++++++++++++++++");
|
||||
}
|
||||
|
||||
// since it's a color definition, it will start a very specific way.
|
||||
String newColorString = colorDefine + " " + colorString;
|
||||
|
||||
int i = 0;
|
||||
while (i != -1) {
|
||||
i = colorDefines.indexOf(newColorString);
|
||||
|
||||
if (i >= 0) {
|
||||
try {
|
||||
int startIndex = i + newColorString.length();
|
||||
int endIndex = colorDefines.indexOf(";", i);
|
||||
|
||||
String colorSubString = colorDefines.substring(startIndex, endIndex)
|
||||
.trim();
|
||||
|
||||
if (colorSubString.startsWith("@")) {
|
||||
// have to recursively get the defined color
|
||||
newColorString = colorDefine + " " + colorSubString.substring(1);
|
||||
i = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
return colorSubString;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return the CSS for the current theme or null. It is important that this is called AFTER GTK has been initialized.
|
||||
@ -872,286 +972,26 @@ GtkPopover .entry {
|
||||
return themeName;
|
||||
}
|
||||
|
||||
private static class CssNode {
|
||||
String label;
|
||||
List<CssAttribute> attributes;
|
||||
|
||||
CssNode(final String label, final List<CssAttribute> attributes) {
|
||||
this.label = label;
|
||||
this.attributes = attributes;
|
||||
// have to strip anything that is not a number.
|
||||
public static
|
||||
int stripNonDigits(final String value) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
String toString() {
|
||||
return label + ' ' + Arrays.toString(attributes.toArray());
|
||||
}
|
||||
}
|
||||
int numberIndex = 0;
|
||||
int length = value.length();
|
||||
|
||||
private static class CssAttribute {
|
||||
String key;
|
||||
String value;
|
||||
|
||||
public
|
||||
CssAttribute(final String key, final String value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sections of text, of the specified CSS nodes.
|
||||
*/
|
||||
private static
|
||||
List<CssNode> getSections(String css, String[] nodes) {
|
||||
// collect a list of all of the sections that have what we are interested in
|
||||
List<CssNode> sections = new ArrayList<CssNode>();
|
||||
|
||||
// now check the css nodes to see if they contain a combination of what we are looking for.
|
||||
colorCheck:
|
||||
for (String node : nodes) {
|
||||
int i = 0;
|
||||
while (i != -1) {
|
||||
i = css.indexOf(node, i);
|
||||
if (i > -1) {
|
||||
int endOfNodeLabels = css.indexOf("{", i);
|
||||
int endOfSection = css.indexOf("}", endOfNodeLabels + 1) + 1;
|
||||
int endOfSectionTest = css.indexOf("}", i) + 1;
|
||||
|
||||
// this makes sure that weird parsing errors don't happen as a result of node keywords appearing in node sections
|
||||
if (endOfSection != endOfSectionTest) {
|
||||
// advance the index
|
||||
i = endOfSection;
|
||||
continue;
|
||||
}
|
||||
|
||||
String nodeLabel = css.substring(i, endOfNodeLabels);
|
||||
|
||||
List<CssAttribute> attributes = new ArrayList<CssAttribute>();
|
||||
|
||||
// split the section into an arrayList, one per item. Split by attribute element
|
||||
String nodeSection = css.substring(endOfNodeLabels, endOfSection);
|
||||
int start = nodeSection.indexOf('{')+1;
|
||||
while (start != -1) {
|
||||
int end = nodeSection.indexOf(';', start);
|
||||
if (end != -1) {
|
||||
int seperator = nodeSection.indexOf(':', start);
|
||||
|
||||
if (seperator < end) {
|
||||
String key = nodeSection.substring(start, seperator).trim();
|
||||
String value = nodeSection.substring(seperator+1, end).trim();
|
||||
|
||||
attributes.add(new CssAttribute(key, value));
|
||||
}
|
||||
start = end+1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sections.add(new CssNode(nodeLabel, attributes));
|
||||
|
||||
// advance the index
|
||||
i = endOfSection;
|
||||
}
|
||||
}
|
||||
while (numberIndex < length && Character.isDigit(value.charAt(numberIndex))) {
|
||||
numberIndex++;
|
||||
}
|
||||
|
||||
if (DEBUG_VERBOSE) {
|
||||
for (CssNode section : sections) {
|
||||
System.err.println("--------------");
|
||||
System.err.println(section);
|
||||
System.err.println("--------------");
|
||||
}
|
||||
String substring = value.substring(0, numberIndex);
|
||||
try {
|
||||
return Integer.parseInt(substring);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
// find an attribute name from the list of sections. The incoming sections will all be related to one of the nodes, we just have to
|
||||
// prioritize them on WHO has the attribute we are looking for.
|
||||
private static
|
||||
String getAttributeFromSections(final List<CssNode> sections, final String[] nodes, final String attributeName) {
|
||||
// a list of sections that contains the exact attribute we are looking for
|
||||
List<CssNode> sectionsWithAttribute = new ArrayList<CssNode>();
|
||||
|
||||
|
||||
for (CssNode cssNode : sections) {
|
||||
for (CssAttribute attribute : cssNode.attributes) {
|
||||
if (attribute.key.equals(attributeName)) {
|
||||
sectionsWithAttribute.add(cssNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we only have 1, then return that one
|
||||
if (sectionsWithAttribute.size() == 1) {
|
||||
CssNode cssNode = sectionsWithAttribute.get(0);
|
||||
for (CssAttribute attribute : cssNode.attributes) {
|
||||
if (attribute.key.equals(attributeName)) {
|
||||
return attribute.value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// now we need to narrow down which sections have the attribute.
|
||||
// This is because a section can override another, and we want to reflect that info.
|
||||
// If a section has more than one node as it's label, then it has a higher priority, and overrides ones that only have a single label.
|
||||
// IE: "GtkPopover .entry" overrides ".entry"
|
||||
|
||||
|
||||
int maxValue = -1;
|
||||
CssNode maxNode = null; // guaranteed to be non-null
|
||||
|
||||
// not the CLEANEST way to do this, but it works by only choosing css nodes that are important
|
||||
for (CssNode cssNode : sectionsWithAttribute) {
|
||||
int count = 0;
|
||||
for (String node : nodes) {
|
||||
String label = cssNode.label;
|
||||
boolean startsWith = label.startsWith(node);
|
||||
boolean contains = label.contains(node);
|
||||
|
||||
if (startsWith) {
|
||||
count+=1;
|
||||
} else if (contains) {
|
||||
// if it has MORE than just one node label (that we care about), it's more important that one that is by itself.
|
||||
count+=2;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > maxValue) {
|
||||
maxValue = count;
|
||||
maxNode = cssNode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// get the attribute from the highest scoring node
|
||||
//noinspection ConstantConditions
|
||||
for (CssAttribute attribute : maxNode.attributes) {
|
||||
if (attribute.key.equals(attributeName)) {
|
||||
return attribute.value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
private static
|
||||
void removeComments(final StringBuilder stringBuilder) {
|
||||
// remove block comments, /* .... */ This can span multiple lines
|
||||
int start = 0;
|
||||
while (start != -1) {
|
||||
// get the start of a comment
|
||||
start = stringBuilder.indexOf("/*", start);
|
||||
|
||||
if (start != -1) {
|
||||
// get the end of a comment
|
||||
int end = stringBuilder.indexOf("*/", start);
|
||||
if (end != -1) {
|
||||
stringBuilder.delete(start, end + 2); // 2 is the size of */
|
||||
|
||||
// sometimes when the comments are removed, there is a trailing newline. remove that too. Works for windows too
|
||||
if (stringBuilder.charAt(start) == '\n') {
|
||||
stringBuilder.delete(start, start + 1);
|
||||
}
|
||||
else {
|
||||
start++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now remove comments that start with // (line MUST start with //)
|
||||
start = 0;
|
||||
while (start != -1) {
|
||||
// get the start of a comment
|
||||
start = stringBuilder.indexOf("//", start);
|
||||
|
||||
if (start != -1) {
|
||||
// the comment is at the start of a line
|
||||
if (start == 0 || stringBuilder.charAt(start-1) == '\n') {
|
||||
// get the end of the comment (the end of the line)
|
||||
int end = stringBuilder.indexOf("\n", start);
|
||||
if (end != -1) {
|
||||
stringBuilder.delete(start, end + 1); // 1 is the size of \n
|
||||
}
|
||||
}
|
||||
|
||||
// sometimes when the comments are removed, there is a trailing newline. remove that too. Works for windows too
|
||||
if (stringBuilder.charAt(start) == '\n') {
|
||||
stringBuilder.delete(start, start + 1);
|
||||
}
|
||||
else if (start > 0){
|
||||
start++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now remove comments that start with # (line MUST start with #)
|
||||
start = 0;
|
||||
while (start != -1) {
|
||||
// get the start of a comment
|
||||
start = stringBuilder.indexOf("#", start);
|
||||
|
||||
if (start != -1) {
|
||||
// the comment is at the start of a line
|
||||
if (start == 0 || stringBuilder.charAt(start-1) == '\n') {
|
||||
// get the end of the comment (the end of the line)
|
||||
int end = stringBuilder.indexOf("\n", start);
|
||||
if (end != -1) {
|
||||
stringBuilder.delete(start, end + 1); // 1 is the size of \n
|
||||
}
|
||||
}
|
||||
|
||||
// sometimes when the comments are removed, there is a trailing newline. remove that too. Works for windows too
|
||||
if (stringBuilder.charAt(start) == '\n') {
|
||||
stringBuilder.delete(start, start + 1);
|
||||
}
|
||||
else if (start > 0){
|
||||
start++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static
|
||||
void injectAdditionalCss(final File parent, final StringBuilder stringBuilder) {
|
||||
// not the BEST way to do this because duplicates are not merged at all.
|
||||
|
||||
int start = 0;
|
||||
while (start != -1) {
|
||||
// now check if it says: @import url("gtk-main.css")
|
||||
start = stringBuilder.indexOf("@import url(", start);
|
||||
|
||||
if (start != -1) {
|
||||
int end = stringBuilder.indexOf("\")", start);
|
||||
if (end != -1) {
|
||||
String url = stringBuilder.substring(start + 13, end);
|
||||
stringBuilder.delete(start, end + 2); // 2 is the size of ")
|
||||
|
||||
if (DEBUG_VERBOSE) {
|
||||
System.err.println("import url: " + url);
|
||||
}
|
||||
try {
|
||||
// now inject the new file where the import command was.
|
||||
File file = new File(parent, url);
|
||||
StringBuilder stringBuilder2 = new StringBuilder((int) (file.length()));
|
||||
FileUtil.read(file, stringBuilder2);
|
||||
|
||||
removeComments(stringBuilder2);
|
||||
|
||||
stringBuilder.insert(start, stringBuilder2);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
535
src/dorkbox/systemTray/util/CssParser.java
Normal file
535
src/dorkbox/systemTray/util/CssParser.java
Normal file
@ -0,0 +1,535 @@
|
||||
package dorkbox.systemTray.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import dorkbox.util.FileUtil;
|
||||
|
||||
/**
|
||||
* A simple, basic CSS parser
|
||||
*/
|
||||
public
|
||||
class CssParser {
|
||||
private static final boolean DEBUG = false;
|
||||
private static final boolean DEBUG_NODES = false;
|
||||
private static final boolean DEBUG_GETTING_ATTRIBUTE_FROM_NODES = false;
|
||||
|
||||
private static
|
||||
String trim(String s) {
|
||||
s = s.replaceAll("\n", "");
|
||||
s = s.replaceAll("\t", "");
|
||||
// shrink all whitespace more than 1 space wide.
|
||||
while (s.contains(" ")) {
|
||||
s = s.replaceAll(" ", " ");
|
||||
}
|
||||
return s.trim();
|
||||
}
|
||||
|
||||
public static
|
||||
class CssNode {
|
||||
public final String label;
|
||||
public final List<CssAttribute> attributes;
|
||||
|
||||
CssNode(final String label, final List<CssAttribute> attributes) {
|
||||
this.label = trim(label);
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
String toString() {
|
||||
return label + "\n\t" + Arrays.toString(attributes.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static
|
||||
class CssAttribute {
|
||||
public final String key;
|
||||
public final String value;
|
||||
|
||||
CssAttribute(final String key, final String value) {
|
||||
this.key = trim(key);
|
||||
this.value = trim(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
String toString() {
|
||||
return key + ':' + value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final CssAttribute attribute = (CssAttribute) o;
|
||||
|
||||
if (key != null ? !key.equals(attribute.key) : attribute.key != null) {
|
||||
return false;
|
||||
}
|
||||
return value != null ? value.equals(attribute.value) : attribute.value == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public
|
||||
int hashCode() {
|
||||
int result = key != null ? key.hashCode() : 0;
|
||||
result = 31 * result + (value != null ? value.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sections of text, of the specified CSS nodes.
|
||||
*
|
||||
* @param css the css text, in it's raw form
|
||||
* @param nodes the section nodes we are interested in (ie: .menuitem, *)
|
||||
* @param states the section state we are interested in (ie: focus, hover, active). Null (or empty list) means no state.
|
||||
*/
|
||||
public static
|
||||
List<CssNode> getSections(String css, String[] nodes, String[] states) {
|
||||
if (states == null) {
|
||||
states = new String[0];
|
||||
}
|
||||
|
||||
// collect a list of all of the sections that have what we are interested in
|
||||
List<CssNode> sections = new ArrayList<CssNode>();
|
||||
|
||||
// now check the css nodes to see if they contain a combination of what we are looking for.
|
||||
for (String node : nodes) {
|
||||
int i = 0;
|
||||
while (i != -1) {
|
||||
i = css.indexOf(node, i);
|
||||
if (i > -1) {
|
||||
int endOfNodeLabels = css.indexOf("{", i);
|
||||
int endOfSection = css.indexOf("}", endOfNodeLabels + 1) + 1;
|
||||
int endOfSectionTest = css.indexOf("}", i) + 1;
|
||||
|
||||
// this makes sure that weird parsing errors don't happen as a result of node keywords appearing in node sections
|
||||
if (endOfSection != endOfSectionTest) {
|
||||
// advance the index
|
||||
i = endOfSection;
|
||||
continue;
|
||||
}
|
||||
|
||||
String nodeLabel = css.substring(i, endOfNodeLabels);
|
||||
|
||||
List<CssAttribute> attributes = new ArrayList<CssAttribute>();
|
||||
|
||||
// split the section into an arrayList, one per item. Split by attribute element
|
||||
String nodeSection = css.substring(endOfNodeLabels, endOfSection);
|
||||
int start = nodeSection.indexOf('{') + 1;
|
||||
while (start != -1) {
|
||||
int end = nodeSection.indexOf(';', start);
|
||||
if (end != -1) {
|
||||
int separator = nodeSection.indexOf(':', start);
|
||||
|
||||
if (separator < end) {
|
||||
String key = nodeSection.substring(start, separator);
|
||||
String value = nodeSection.substring(separator + 1, end);
|
||||
attributes.add(new CssAttribute(key, value));
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if the label contains ',' this means that MORE that one CssNode has the same attributes. We want to split that up.
|
||||
int multiIndex = nodeLabel.indexOf(',');
|
||||
if (multiIndex != -1) {
|
||||
multiIndex = 0;
|
||||
while (multiIndex != -1) {
|
||||
int multiEndIndex = nodeLabel.indexOf(',', multiIndex);
|
||||
if (multiEndIndex != -1) {
|
||||
String newLabel = nodeLabel.substring(multiIndex, multiEndIndex);
|
||||
|
||||
sections.add(new CssNode(newLabel, attributes));
|
||||
multiIndex = multiEndIndex+1;
|
||||
} else {
|
||||
// now add the last part of the label.
|
||||
String newLabel = nodeLabel.substring(multiIndex);
|
||||
sections.add(new CssNode(newLabel, attributes));
|
||||
multiIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// we are the only one with these attributes
|
||||
sections.add(new CssNode(nodeLabel, attributes));
|
||||
}
|
||||
|
||||
// advance the index
|
||||
i = endOfSection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// our sections can ONLY contain what we are looking for, as a word.
|
||||
for (Iterator<CssNode> iterator = sections.iterator(); iterator.hasNext(); ) {
|
||||
final CssNode section = iterator.next();
|
||||
String label = section.label;
|
||||
boolean canSave = false;
|
||||
|
||||
if (!section.attributes.isEmpty()) {
|
||||
main:
|
||||
for (String node : nodes) {
|
||||
if (label.equals(node)) {
|
||||
// exactly what our node is
|
||||
canSave = true;
|
||||
break;
|
||||
}
|
||||
if (label.length() > node.length() && label.startsWith(node)) {
|
||||
// a combination of our node + MAYBE some other node
|
||||
int index = node.length();
|
||||
label = trim(label.substring(index));
|
||||
|
||||
if (label.charAt(0) == '>') {
|
||||
// if it's an override, we have to check what it overrides.
|
||||
label = label.substring(1);
|
||||
}
|
||||
|
||||
// then, this MUST be one of our other nodes (that we are looking for, otherwise remove this section)
|
||||
for (String n : nodes) {
|
||||
//noinspection StringEquality
|
||||
if (n != node && label.startsWith(n)) {
|
||||
canSave = true;
|
||||
break main;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (canSave) {
|
||||
// if this section is for a state we DO NOT care about, remove it
|
||||
int stateIndex = label.lastIndexOf(':');
|
||||
if (stateIndex != -1) {
|
||||
String stateValue = label.substring(stateIndex+1);
|
||||
boolean saveState = false;
|
||||
for (String state : states) {
|
||||
if (stateValue.equals(state)) {
|
||||
// this is a state we care about
|
||||
saveState = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!saveState) {
|
||||
canSave = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!canSave) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// now merge all nodes that have the same labels.
|
||||
for (Iterator<CssNode> iterator = sections.iterator(); iterator.hasNext(); ) {
|
||||
final CssNode section = iterator.next();
|
||||
|
||||
if (section != null) {
|
||||
String label = section.label;
|
||||
|
||||
for (int i = 0; i < sections.size(); i++) {
|
||||
final CssNode section2 = sections.get(i);
|
||||
if (section != section2 && section2 != null && label.equals(section2.label)) {
|
||||
sections.set(i, null);
|
||||
|
||||
// now merge both lists.
|
||||
for (CssAttribute attribute : section.attributes) {
|
||||
for (Iterator<CssAttribute> iterator2 = section2.attributes.iterator(); iterator2.hasNext(); ) {
|
||||
final CssAttribute attribute2 = iterator2.next();
|
||||
|
||||
if (attribute.equals(attribute2)) {
|
||||
iterator2.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now both lists are unique.
|
||||
section.attributes.addAll(section2.attributes);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// clean up the (possible) null entries.
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// final cleanup loop
|
||||
for (Iterator<CssNode> iterator = sections.iterator(); iterator.hasNext(); ) {
|
||||
final CssNode section = iterator.next();
|
||||
|
||||
if (section.attributes.isEmpty()) {
|
||||
iterator.remove();
|
||||
} else {
|
||||
for (Iterator<CssAttribute> iterator1 = section.attributes.iterator(); iterator1.hasNext(); ) {
|
||||
final CssAttribute attribute = iterator1.next();
|
||||
|
||||
if (attribute == null) {
|
||||
iterator1.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (DEBUG_NODES) {
|
||||
for (CssNode section : sections) {
|
||||
System.err.println("--------------");
|
||||
System.err.println(section);
|
||||
System.err.println("--------------");
|
||||
}
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* find an attribute name from the list of sections. The incoming sections will all be related to one of the nodes, we prioritize
|
||||
* them on WHO has the attribute we are looking for.
|
||||
*
|
||||
* @param sections the css sections
|
||||
* @param nodes the nodes (array) of strings (in descending importance) of the section titles we are looking for
|
||||
* @param attributeName the name of the attribute we are looking for.
|
||||
* @param equalsOrContained true if we want to EXACT match, false if the attribute key can contain what we are looking for.
|
||||
*
|
||||
* @return the attribute value, if found
|
||||
*/
|
||||
@SuppressWarnings("Duplicates")
|
||||
public static
|
||||
String getAttributeFromSections(final List<CssNode> sections,
|
||||
final String[] nodes,
|
||||
final String attributeName,
|
||||
boolean equalsOrContained) {
|
||||
|
||||
// a list of sections that contains the exact attribute we are looking for
|
||||
List<CssNode> sectionsWithAttribute = new ArrayList<CssNode>();
|
||||
for (CssNode cssNode : sections) {
|
||||
for (CssAttribute attribute : cssNode.attributes) {
|
||||
if (equalsOrContained) {
|
||||
if (attribute.key.equals(attributeName)) {
|
||||
sectionsWithAttribute.add(cssNode);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (attribute.key.contains(attributeName)) {
|
||||
sectionsWithAttribute.add(cssNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (DEBUG_GETTING_ATTRIBUTE_FROM_NODES) {
|
||||
System.err.println("--------------");
|
||||
System.err.println("Cleaned Sections");
|
||||
System.err.println("--------------");
|
||||
for (CssNode section : sectionsWithAttribute) {
|
||||
System.err.println("--------------");
|
||||
System.err.println(section);
|
||||
System.err.println("--------------");
|
||||
}
|
||||
}
|
||||
|
||||
// if we only have 1, then return that one
|
||||
if (sectionsWithAttribute.size() == 1) {
|
||||
CssNode cssNode = sectionsWithAttribute.get(0);
|
||||
for (CssAttribute attribute : cssNode.attributes) {
|
||||
if (equalsOrContained) {
|
||||
if (attribute.key.equals(attributeName)) {
|
||||
return attribute.value;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (attribute.key.contains(attributeName)) {
|
||||
return attribute.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// now we need to narrow down which sections have the attribute.
|
||||
// This is because a section can override another, and we want to reflect that info.
|
||||
// If a section has more than one node as it's label, then it has a higher priority, and overrides ones that only have a single label.
|
||||
// IE: "GtkPopover .entry" overrides ".entry"
|
||||
|
||||
|
||||
int maxValue = -1;
|
||||
CssNode maxNode = null; // guaranteed to be non-null
|
||||
|
||||
// not the CLEANEST way to do this, but it works by only choosing css nodes that are important
|
||||
for (CssNode cssNode : sectionsWithAttribute) {
|
||||
int count = 0;
|
||||
for (String node : nodes) {
|
||||
String label = cssNode.label;
|
||||
boolean startsWith = label.startsWith(node);
|
||||
// make sure the . version (if we don't have it specified) isn't counted twice.
|
||||
boolean contains = label.contains(node) && !label.contains('.' + node);
|
||||
|
||||
if (startsWith) {
|
||||
count++;
|
||||
}
|
||||
else if (contains) {
|
||||
// if it has MORE than just one node label (that we care about), it's more important that one that is by itself.
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > maxValue) {
|
||||
maxValue = count;
|
||||
maxNode = cssNode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// get the attribute from the highest scoring node
|
||||
//noinspection ConstantConditions
|
||||
for (CssAttribute attribute : maxNode.attributes) {
|
||||
if (equalsOrContained) {
|
||||
if (attribute.key.equals(attributeName)) {
|
||||
return attribute.value;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (attribute.key.contains(attributeName)) {
|
||||
return attribute.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static
|
||||
void injectAdditionalCss(final File parent, final StringBuilder stringBuilder) {
|
||||
// not the BEST way to do this because duplicates are not merged at all.
|
||||
|
||||
int start = 0;
|
||||
while (start != -1) {
|
||||
// now check if it says: @import url("gtk-main.css")
|
||||
start = stringBuilder.indexOf("@import url(", start);
|
||||
|
||||
if (start != -1) {
|
||||
int end = stringBuilder.indexOf("\")", start);
|
||||
if (end != -1) {
|
||||
String url = stringBuilder.substring(start + 13, end);
|
||||
stringBuilder.delete(start, end + 2); // 2 is the size of ")
|
||||
|
||||
if (DEBUG) {
|
||||
System.err.println("import url: " + url);
|
||||
}
|
||||
try {
|
||||
// now inject the new file where the import command was.
|
||||
File file = new File(parent, url);
|
||||
StringBuilder stringBuilder2 = new StringBuilder((int) (file.length()));
|
||||
FileUtil.read(file, stringBuilder2);
|
||||
|
||||
removeComments(stringBuilder2);
|
||||
|
||||
stringBuilder.insert(start, stringBuilder2);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
public static
|
||||
void removeComments(final StringBuilder stringBuilder) {
|
||||
// remove block comments, /* .... */ This can span multiple lines
|
||||
int start = 0;
|
||||
while (start != -1) {
|
||||
// get the start of a comment
|
||||
start = stringBuilder.indexOf("/*", start);
|
||||
|
||||
if (start != -1) {
|
||||
// get the end of a comment
|
||||
int end = stringBuilder.indexOf("*/", start);
|
||||
if (end != -1) {
|
||||
stringBuilder.delete(start, end + 2); // 2 is the size of */
|
||||
|
||||
// sometimes when the comments are removed, there is a trailing newline. remove that too. Works for windows too
|
||||
if (stringBuilder.charAt(start) == '\n') {
|
||||
stringBuilder.delete(start, start + 1);
|
||||
}
|
||||
else {
|
||||
start++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now remove comments that start with // (line MUST start with //)
|
||||
start = 0;
|
||||
while (start != -1) {
|
||||
// get the start of a comment
|
||||
start = stringBuilder.indexOf("//", start);
|
||||
|
||||
if (start != -1) {
|
||||
// the comment is at the start of a line
|
||||
if (start == 0 || stringBuilder.charAt(start - 1) == '\n') {
|
||||
// get the end of the comment (the end of the line)
|
||||
int end = stringBuilder.indexOf("\n", start);
|
||||
if (end != -1) {
|
||||
stringBuilder.delete(start, end + 1); // 1 is the size of \n
|
||||
}
|
||||
}
|
||||
|
||||
// sometimes when the comments are removed, there is a trailing newline. remove that too. Works for windows too
|
||||
if (stringBuilder.charAt(start) == '\n') {
|
||||
stringBuilder.delete(start, start + 1);
|
||||
}
|
||||
else if (start > 0) {
|
||||
start++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now remove comments that start with # (line MUST start with #)
|
||||
start = 0;
|
||||
while (start != -1) {
|
||||
// get the start of a comment
|
||||
start = stringBuilder.indexOf("#", start);
|
||||
|
||||
if (start != -1) {
|
||||
// the comment is at the start of a line
|
||||
if (start == 0 || stringBuilder.charAt(start - 1) == '\n') {
|
||||
// get the end of the comment (the end of the line)
|
||||
int end = stringBuilder.indexOf("\n", start);
|
||||
if (end != -1) {
|
||||
stringBuilder.delete(start, end + 1); // 1 is the size of \n
|
||||
}
|
||||
}
|
||||
|
||||
// sometimes when the comments are removed, there is a trailing newline. remove that too. Works for windows too
|
||||
if (stringBuilder.charAt(start) == '\n') {
|
||||
stringBuilder.delete(start, start + 1);
|
||||
}
|
||||
else if (start > 0) {
|
||||
start++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user