From 951a600f0bc2dfd94005c82b240c5d267179e3d7 Mon Sep 17 00:00:00 2001 From: nathan Date: Wed, 7 Jun 2017 14:17:49 +0200 Subject: [PATCH] Moved css parsing to it's own class, code polish, method refactoring for legibilityty --- .../systemTray/jna/linux/GtkTheme.java | 570 +++++++----------- src/dorkbox/systemTray/util/CssParser.java | 535 ++++++++++++++++ 2 files changed, 740 insertions(+), 365 deletions(-) create mode 100644 src/dorkbox/systemTray/util/CssParser.java diff --git a/src/dorkbox/systemTray/jna/linux/GtkTheme.java b/src/dorkbox/systemTray/jna/linux/GtkTheme.java index 37dc52e..a999d0c 100644 --- a/src/dorkbox/systemTray/jna/linux/GtkTheme.java +++ b/src/dorkbox/systemTray/jna/linux/GtkTheme.java @@ -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. *

* 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 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 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 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 sections = getSections(css, nodes); - - // - String colorString = getAttributeFromSections(sections, nodes, "color"); + List 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 attributes; - - CssNode(final String label, final List 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 getSections(String css, String[] nodes) { - // collect a list of all of the sections that have what we are interested in - List sections = new ArrayList(); - - // 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 attributes = new ArrayList(); - - // 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 sections, final String[] nodes, final String attributeName) { - // a list of sections that contains the exact attribute we are looking for - List sectionsWithAttribute = new ArrayList(); - - - 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; } } diff --git a/src/dorkbox/systemTray/util/CssParser.java b/src/dorkbox/systemTray/util/CssParser.java new file mode 100644 index 0000000..488b665 --- /dev/null +++ b/src/dorkbox/systemTray/util/CssParser.java @@ -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 attributes; + + CssNode(final String label, final List 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 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 sections = new ArrayList(); + + // 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 attributes = new ArrayList(); + + // 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 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 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 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 iterator = sections.iterator(); iterator.hasNext(); ) { + final CssNode section = iterator.next(); + + if (section.attributes.isEmpty()) { + iterator.remove(); + } else { + for (Iterator 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 sections, + final String[] nodes, + final String attributeName, + boolean equalsOrContained) { + + // a list of sections that contains the exact attribute we are looking for + List sectionsWithAttribute = new ArrayList(); + 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++; + } + } + } + } +}