Add battery bar colour expressions
This commit is contained in:
+83
-10
@@ -7,40 +7,66 @@ import java.util.Locale;
|
||||
|
||||
import se.ajpanton.statusbartweak.shell.settings.BatteryBarStyle;
|
||||
|
||||
final class ClockColorResolver {
|
||||
public final class ClockColorResolver {
|
||||
private static final int CHANNEL_ALPHA = 0;
|
||||
private static final int CHANNEL_RED = 1;
|
||||
private static final int CHANNEL_GREEN = 2;
|
||||
private static final int CHANNEL_BLUE = 3;
|
||||
private static final int CHANNEL_COUNT = 4;
|
||||
|
||||
String resolveColorHex(
|
||||
public String resolveColorHex(
|
||||
Context context,
|
||||
String expression,
|
||||
int referenceColor,
|
||||
int fallbackColor
|
||||
) {
|
||||
int resolved = resolveColor(context, expression, referenceColor, fallbackColor);
|
||||
return colorHex(resolved);
|
||||
}
|
||||
|
||||
public String resolveColorHex(
|
||||
Context context,
|
||||
String expression,
|
||||
int referenceColor,
|
||||
int fallbackColor,
|
||||
Options options
|
||||
) {
|
||||
int resolved = resolveColor(context, expression, referenceColor, fallbackColor, options);
|
||||
return colorHex(resolved);
|
||||
}
|
||||
|
||||
private String colorHex(int resolved) {
|
||||
if (alpha(resolved) >= 255) {
|
||||
return String.format(Locale.ROOT, "#%06X", resolved & 0xFFFFFF);
|
||||
}
|
||||
return String.format(Locale.ROOT, "#%08X", resolved);
|
||||
}
|
||||
|
||||
int resolveColor(
|
||||
public int resolveColor(
|
||||
Context context,
|
||||
String expression,
|
||||
int referenceColor,
|
||||
int fallbackColor
|
||||
) {
|
||||
return resolveColor(context, expression, referenceColor, fallbackColor, Options.clock());
|
||||
}
|
||||
|
||||
public int resolveColor(
|
||||
Context context,
|
||||
String expression,
|
||||
int referenceColor,
|
||||
int fallbackColor,
|
||||
Options options
|
||||
) {
|
||||
if (isEmpty(expression)) {
|
||||
return fallbackColor;
|
||||
}
|
||||
Options resolvedOptions = options != null ? options : Options.clock();
|
||||
VariantExpressions variants = splitVariants(expression);
|
||||
Value bright = evaluateExpression(context, variants.brightExpression, null, fallbackColor);
|
||||
Value bright = evaluateExpression(context, variants.brightExpression, null, fallbackColor, resolvedOptions);
|
||||
Value dark = variants.darkExpression.isEmpty()
|
||||
? bright
|
||||
: evaluateExpression(context, variants.darkExpression, bright, fallbackColor);
|
||||
: evaluateExpression(context, variants.darkExpression, bright, fallbackColor, resolvedOptions);
|
||||
return toColor(isDark(referenceColor) ? dark : bright);
|
||||
}
|
||||
|
||||
@@ -48,7 +74,8 @@ final class ClockColorResolver {
|
||||
Context context,
|
||||
String expression,
|
||||
Value inherited,
|
||||
int fallbackColor
|
||||
int fallbackColor,
|
||||
Options options
|
||||
) {
|
||||
ArrayList<String> tokens = tokenize(expression);
|
||||
ArrayList<Value> stack = new ArrayList<>();
|
||||
@@ -56,7 +83,7 @@ final class ClockColorResolver {
|
||||
stack.add(inherited.copy());
|
||||
}
|
||||
for (String token : tokens) {
|
||||
applyToken(context, stack, token, fallbackColor);
|
||||
applyToken(context, stack, token, fallbackColor, options);
|
||||
}
|
||||
if (stack.isEmpty()) {
|
||||
throw new IllegalArgumentException("Empty colour expression");
|
||||
@@ -64,7 +91,13 @@ final class ClockColorResolver {
|
||||
return top(stack).copy();
|
||||
}
|
||||
|
||||
private void applyToken(Context context, ArrayList<Value> stack, String token, int fallbackColor) {
|
||||
private void applyToken(
|
||||
Context context,
|
||||
ArrayList<Value> stack,
|
||||
String token,
|
||||
int fallbackColor,
|
||||
Options options
|
||||
) {
|
||||
if (isEmpty(token)) {
|
||||
return;
|
||||
}
|
||||
@@ -73,7 +106,7 @@ final class ClockColorResolver {
|
||||
applyOperation(stack, operation);
|
||||
return;
|
||||
}
|
||||
stack.add(parseOperand(context, token, fallbackColor));
|
||||
stack.add(parseOperand(context, token, fallbackColor, options));
|
||||
}
|
||||
|
||||
private Operation parseOperation(String token) {
|
||||
@@ -244,12 +277,15 @@ final class ClockColorResolver {
|
||||
stack.add(Value.vector(result));
|
||||
}
|
||||
|
||||
private Value parseOperand(Context context, String token, int fallbackColor) {
|
||||
private Value parseOperand(Context context, String token, int fallbackColor, Options options) {
|
||||
String value = token != null ? token.trim() : "";
|
||||
if (value.isEmpty()) {
|
||||
throw new IllegalArgumentException("Empty colour token");
|
||||
}
|
||||
if ("#batterybar".equalsIgnoreCase(value)) {
|
||||
if (options == null || !options.allowBatteryBarOperand) {
|
||||
throw new IllegalArgumentException("#batterybar is not supported here");
|
||||
}
|
||||
return Value.vector(vectorForColor(BatteryBarStyle.resolveCurrentColor(context, fallbackColor, fallbackColor)));
|
||||
}
|
||||
if (value.charAt(0) == '#') {
|
||||
@@ -258,6 +294,9 @@ final class ClockColorResolver {
|
||||
if (value.charAt(0) == '[') {
|
||||
return Value.vector(parseVector(value));
|
||||
}
|
||||
if (options != null && options.allowBareHexOperand && looksLikeBareHex(value)) {
|
||||
return Value.vector(vectorForColor(parseHexColor("#" + value)));
|
||||
}
|
||||
try {
|
||||
return Value.number(Double.parseDouble(value));
|
||||
} catch (NumberFormatException e) {
|
||||
@@ -265,6 +304,22 @@ final class ClockColorResolver {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean looksLikeBareHex(String value) {
|
||||
int length = value != null ? value.length() : 0;
|
||||
if (length != 3 && length != 4 && length != 6 && length != 8) {
|
||||
return false;
|
||||
}
|
||||
boolean hasHexLetter = false;
|
||||
for (int i = 0; i < length; i++) {
|
||||
char c = value.charAt(i);
|
||||
if (!isHexDigit(c)) {
|
||||
return false;
|
||||
}
|
||||
hasHexLetter |= (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||
}
|
||||
return length != 3 || hasHexLetter;
|
||||
}
|
||||
|
||||
private double[] parseVector(String token) {
|
||||
String value = token != null ? token.trim() : "";
|
||||
if (!value.startsWith("[") || !value.endsWith("]")) {
|
||||
@@ -608,4 +663,22 @@ final class ClockColorResolver {
|
||||
return Math.max(0d, Math.min(255d, value));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Options {
|
||||
private final boolean allowBatteryBarOperand;
|
||||
private final boolean allowBareHexOperand;
|
||||
|
||||
private Options(boolean allowBatteryBarOperand, boolean allowBareHexOperand) {
|
||||
this.allowBatteryBarOperand = allowBatteryBarOperand;
|
||||
this.allowBareHexOperand = allowBareHexOperand;
|
||||
}
|
||||
|
||||
public static Options clock() {
|
||||
return new Options(true, false);
|
||||
}
|
||||
|
||||
public static Options batteryBar() {
|
||||
return new Options(false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+57
-346
@@ -1,394 +1,105 @@
|
||||
package se.ajpanton.statusbartweak.shell.settings;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import se.ajpanton.statusbartweak.runtime.features.clock.ClockColorResolver;
|
||||
|
||||
public final class ColourExpressionSupport {
|
||||
private static final Pattern OPERATION_PATTERN =
|
||||
Pattern.compile("([+\\-*/])\\s*(" + hexOperandPattern() + "|\\d+(?:\\.\\d+)?|\\.\\d+)");
|
||||
private static final Pattern LEADING_HEX_PATTERN =
|
||||
Pattern.compile("(?i)^#?(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})");
|
||||
private static final ClockColorResolver RESOLVER = new ClockColorResolver();
|
||||
private static final ClockColorResolver.Options OPTIONS = ClockColorResolver.Options.batteryBar();
|
||||
|
||||
private ColourExpressionSupport() {
|
||||
}
|
||||
|
||||
public static String normaliseExpression(String raw, boolean assumeHashPrefix) {
|
||||
if (raw == null) {
|
||||
return "";
|
||||
}
|
||||
String value = raw.trim();
|
||||
String value = normaliseSeparators(raw);
|
||||
if (value.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
int variantSplit = findVariantSplit(value);
|
||||
String left = value;
|
||||
String right = null;
|
||||
if (variantSplit >= 0) {
|
||||
left = value.substring(0, variantSplit).trim();
|
||||
right = value.substring(variantSplit + 1).trim();
|
||||
}
|
||||
if (!isValidSegment(left, assumeHashPrefix, false, Color.WHITE)) {
|
||||
try {
|
||||
RESOLVER.resolveColor(null, value, Color.WHITE, Color.WHITE, OPTIONS);
|
||||
RESOLVER.resolveColor(null, value, Color.BLACK, Color.WHITE, OPTIONS);
|
||||
return value;
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return "";
|
||||
}
|
||||
if (right != null && !isValidSegment(right, assumeHashPrefix, true, Color.WHITE)) {
|
||||
return "";
|
||||
}
|
||||
if (right == null) {
|
||||
return left;
|
||||
}
|
||||
return left + "|" + right;
|
||||
}
|
||||
|
||||
public static int resolveColor(String raw,
|
||||
int referenceColor,
|
||||
int fallbackColor,
|
||||
boolean assumeHashPrefix) {
|
||||
String value = normaliseExpression(raw, assumeHashPrefix);
|
||||
public static int resolveColor(
|
||||
String raw,
|
||||
int referenceColor,
|
||||
int fallbackColor,
|
||||
boolean assumeHashPrefix
|
||||
) {
|
||||
String value = normaliseSeparators(raw);
|
||||
if (value.isEmpty()) {
|
||||
return fallbackColor;
|
||||
}
|
||||
int variantSplit = findVariantSplit(value);
|
||||
if (variantSplit < 0) {
|
||||
return resolveSegment(value, fallbackColor, assumeHashPrefix, false);
|
||||
}
|
||||
String left = value.substring(0, variantSplit).trim();
|
||||
String right = value.substring(variantSplit + 1).trim();
|
||||
int brightVariant = resolveSegment(left, fallbackColor, assumeHashPrefix, false);
|
||||
if (!isDark(referenceColor)) {
|
||||
return brightVariant;
|
||||
}
|
||||
return resolveSegment(right, brightVariant, assumeHashPrefix, true);
|
||||
}
|
||||
|
||||
public static String resolveColorHex(String raw,
|
||||
int referenceColor,
|
||||
int fallbackColor,
|
||||
boolean assumeHashPrefix) {
|
||||
int resolved = resolveColor(raw, referenceColor, fallbackColor, assumeHashPrefix);
|
||||
int alpha = Color.alpha(resolved);
|
||||
if (alpha >= 255) {
|
||||
return String.format(Locale.ROOT, "#%06X", resolved & 0xFFFFFF);
|
||||
}
|
||||
return String.format(Locale.ROOT, "#%08X", resolved);
|
||||
}
|
||||
|
||||
private static boolean isValidSegment(String segment,
|
||||
boolean assumeHashPrefix,
|
||||
boolean allowInheritedBase,
|
||||
int baseColor) {
|
||||
if (TextUtils.isEmpty(segment)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
resolveSegment(segment, baseColor, assumeHashPrefix, allowInheritedBase);
|
||||
return true;
|
||||
return RESOLVER.resolveColor(null, value, referenceColor, fallbackColor, OPTIONS);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return false;
|
||||
return fallbackColor;
|
||||
}
|
||||
}
|
||||
|
||||
private static int resolveSegment(String segment,
|
||||
int baseColor,
|
||||
boolean assumeHashPrefix,
|
||||
boolean allowInheritedBase) {
|
||||
String value = segment != null ? segment.trim() : "";
|
||||
public static String resolveColorHex(
|
||||
String raw,
|
||||
int referenceColor,
|
||||
int fallbackColor,
|
||||
boolean assumeHashPrefix
|
||||
) {
|
||||
String value = normaliseSeparators(raw);
|
||||
if (value.isEmpty()) {
|
||||
throw new IllegalArgumentException("Empty colour segment");
|
||||
}
|
||||
int index = 0;
|
||||
boolean invert = false;
|
||||
if (value.regionMatches(true, 0, "inv", 0, 3)) {
|
||||
invert = true;
|
||||
index = 3;
|
||||
}
|
||||
double[] channels;
|
||||
double alpha;
|
||||
String remainder;
|
||||
if (startsWithOperator(value, index)) {
|
||||
if (!allowInheritedBase) {
|
||||
throw new IllegalArgumentException("Missing starting colour");
|
||||
}
|
||||
channels = channelsFor(baseColor);
|
||||
alpha = Color.alpha(baseColor);
|
||||
remainder = value.substring(index).trim();
|
||||
} else {
|
||||
LeadingColour leading = parseLeadingColour(value, index, assumeHashPrefix);
|
||||
channels = channelsFor(leading.color);
|
||||
alpha = Color.alpha(leading.color);
|
||||
remainder = value.substring(leading.endIndex).trim();
|
||||
}
|
||||
if (invert) {
|
||||
invertChannels(channels);
|
||||
}
|
||||
alpha = applyOperations(channels, alpha, remainder);
|
||||
return colorFromChannels(channels, alpha);
|
||||
}
|
||||
|
||||
private static LeadingColour parseLeadingColour(String value,
|
||||
int start,
|
||||
boolean assumeHashPrefix) {
|
||||
String remainder = value.substring(start).trim();
|
||||
Matcher matcher = LEADING_HEX_PATTERN.matcher(remainder);
|
||||
if (!matcher.find()) {
|
||||
throw new IllegalArgumentException("Missing starting colour");
|
||||
}
|
||||
String token = matcher.group();
|
||||
if (assumeHashPrefix && token.charAt(0) != '#') {
|
||||
token = "#" + token;
|
||||
}
|
||||
int color = parseHexColor(token, assumeHashPrefix);
|
||||
return new LeadingColour(color, start + matcher.end());
|
||||
}
|
||||
|
||||
private static double applyOperations(double[] channels, double alpha, String remainder) {
|
||||
int index = 0;
|
||||
while (index < remainder.length()) {
|
||||
while (index < remainder.length() && Character.isWhitespace(remainder.charAt(index))) {
|
||||
index++;
|
||||
}
|
||||
if (index >= remainder.length()) {
|
||||
break;
|
||||
}
|
||||
Matcher matcher = OPERATION_PATTERN.matcher(remainder);
|
||||
matcher.region(index, remainder.length());
|
||||
if (!matcher.lookingAt()) {
|
||||
throw new IllegalArgumentException("Invalid colour operation");
|
||||
}
|
||||
char operator = matcher.group(1).charAt(0);
|
||||
String operandText = matcher.group(2);
|
||||
alpha = applyOperation(channels, alpha, operator, operandText);
|
||||
index = matcher.end();
|
||||
}
|
||||
return alpha;
|
||||
}
|
||||
|
||||
private static double applyOperation(double[] channels, double alpha, char operator, String operandText) {
|
||||
Operand operand = parseOperand(operandText);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
double value = channels[i];
|
||||
double rhs = operand.rgb[i];
|
||||
if (operand.isHex && (operator == '*' || operator == '/')) {
|
||||
rhs = rhs / 255d;
|
||||
}
|
||||
switch (operator) {
|
||||
case '+':
|
||||
channels[i] = value + rhs;
|
||||
break;
|
||||
case '-':
|
||||
channels[i] = value - rhs;
|
||||
break;
|
||||
case '*':
|
||||
channels[i] = value * rhs;
|
||||
break;
|
||||
case '/':
|
||||
channels[i] = rhs == 0d ? value : value / rhs;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown operator");
|
||||
}
|
||||
}
|
||||
if (operand.hasAlpha) {
|
||||
double rhs = operand.alpha;
|
||||
if (operand.isHex && (operator == '*' || operator == '/')) {
|
||||
rhs = rhs / 255d;
|
||||
}
|
||||
switch (operator) {
|
||||
case '+':
|
||||
alpha = alpha + rhs;
|
||||
break;
|
||||
case '-':
|
||||
alpha = alpha - rhs;
|
||||
break;
|
||||
case '*':
|
||||
alpha = alpha * rhs;
|
||||
break;
|
||||
case '/':
|
||||
alpha = rhs == 0d ? alpha : alpha / rhs;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown operator");
|
||||
}
|
||||
}
|
||||
return alpha;
|
||||
}
|
||||
|
||||
private static Operand parseOperand(String operandText) {
|
||||
String value = operandText != null ? operandText.trim() : "";
|
||||
if (value.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing operand");
|
||||
}
|
||||
if (value.charAt(0) == '#') {
|
||||
return parseHexOperand(value);
|
||||
}
|
||||
double scalar;
|
||||
try {
|
||||
scalar = Double.parseDouble(value);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid operand", e);
|
||||
}
|
||||
return new Operand(new double[] { scalar, scalar, scalar }, 0d, false, false);
|
||||
}
|
||||
|
||||
private static int parseHexColor(String raw, boolean assumeHashPrefix) {
|
||||
String value = raw != null ? raw.trim() : "";
|
||||
if (value.isEmpty()) {
|
||||
throw new IllegalArgumentException("Empty colour");
|
||||
}
|
||||
if (assumeHashPrefix && value.charAt(0) != '#') {
|
||||
value = "#" + value;
|
||||
}
|
||||
if (value.charAt(0) != '#') {
|
||||
throw new IllegalArgumentException("Expected colour literal");
|
||||
}
|
||||
String hex = value.substring(1);
|
||||
if (hex.length() == 3) {
|
||||
hex = expandShortHex(hex);
|
||||
} else if (hex.length() == 4) {
|
||||
hex = expandShortHex(hex);
|
||||
} else if (hex.length() != 6 && hex.length() != 8) {
|
||||
throw new IllegalArgumentException("Unsupported colour length");
|
||||
return hexFor(fallbackColor);
|
||||
}
|
||||
try {
|
||||
return Color.parseColor("#" + hex);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("Invalid colour", e);
|
||||
return RESOLVER.resolveColorHex(null, value, referenceColor, fallbackColor, OPTIONS);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return hexFor(fallbackColor);
|
||||
}
|
||||
}
|
||||
|
||||
private static String expandShortHex(String hex) {
|
||||
StringBuilder out = new StringBuilder(hex.length() * 2);
|
||||
for (int i = 0; i < hex.length(); i++) {
|
||||
char c = hex.charAt(i);
|
||||
out.append(c).append(c);
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static double[] channelsFor(int color) {
|
||||
return new double[] {
|
||||
Color.red(color),
|
||||
Color.green(color),
|
||||
Color.blue(color)
|
||||
};
|
||||
}
|
||||
|
||||
private static void invertChannels(double[] channels) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
channels[i] = 255d - channels[i];
|
||||
}
|
||||
}
|
||||
|
||||
private static int colorFromChannels(double[] channels, double alpha) {
|
||||
int red = clampChannel(channels[0]);
|
||||
int green = clampChannel(channels[1]);
|
||||
int blue = clampChannel(channels[2]);
|
||||
return Color.argb(clampChannel(alpha), red, green, blue);
|
||||
}
|
||||
|
||||
private static Operand parseHexOperand(String raw) {
|
||||
private static String normaliseSeparators(String raw) {
|
||||
String value = raw != null ? raw.trim() : "";
|
||||
if (value.isEmpty() || value.charAt(0) != '#') {
|
||||
throw new IllegalArgumentException("Expected colour literal");
|
||||
if (value.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
String hex = value.substring(1);
|
||||
if (hex.length() == 3 || hex.length() == 4) {
|
||||
hex = expandShortHex(hex);
|
||||
} else if (hex.length() != 6 && hex.length() != 8) {
|
||||
throw new IllegalArgumentException("Unsupported colour length");
|
||||
int split = findTopLevelPipe(value);
|
||||
if (split < 0) {
|
||||
return value;
|
||||
}
|
||||
boolean hasAlpha = hex.length() == 8;
|
||||
int offset = hasAlpha ? 2 : 0;
|
||||
double alpha = 0d;
|
||||
if (hasAlpha) {
|
||||
alpha = parseHexByte(hex.substring(0, 2));
|
||||
}
|
||||
double[] rgb = new double[] {
|
||||
parseHexByte(hex.substring(offset, offset + 2)),
|
||||
parseHexByte(hex.substring(offset + 2, offset + 4)),
|
||||
parseHexByte(hex.substring(offset + 4, offset + 6))
|
||||
};
|
||||
return new Operand(rgb, alpha, hasAlpha, true);
|
||||
return value.substring(0, split).trim() + " ; " + value.substring(split + 1).trim();
|
||||
}
|
||||
|
||||
private static double parseHexByte(String value) {
|
||||
return Integer.parseInt(value, 16);
|
||||
}
|
||||
|
||||
private static int clampChannel(double value) {
|
||||
if (Double.isNaN(value) || Double.isInfinite(value)) {
|
||||
return 0;
|
||||
}
|
||||
if (value <= 0d) {
|
||||
return 0;
|
||||
}
|
||||
if (value >= 255d) {
|
||||
return 255;
|
||||
}
|
||||
return (int) Math.round(value);
|
||||
}
|
||||
|
||||
private static boolean startsWithOperator(String value, int start) {
|
||||
if (start < 0 || start >= value.length()) {
|
||||
return false;
|
||||
}
|
||||
char c = value.charAt(start);
|
||||
return c == '+' || c == '-' || c == '*' || c == '/';
|
||||
}
|
||||
|
||||
private static int findVariantSplit(String value) {
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
return -1;
|
||||
}
|
||||
boolean quoted = false;
|
||||
private static int findTopLevelPipe(String value) {
|
||||
boolean inVector = false;
|
||||
int split = -1;
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char c = value.charAt(i);
|
||||
if (c == '\'') {
|
||||
quoted = !quoted;
|
||||
if (inVector) {
|
||||
if (c == ']') {
|
||||
inVector = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!quoted && c == '|') {
|
||||
return i;
|
||||
if (c == '[') {
|
||||
inVector = true;
|
||||
continue;
|
||||
}
|
||||
if (c == '|') {
|
||||
if (split >= 0) {
|
||||
return -1;
|
||||
}
|
||||
split = i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
return split;
|
||||
}
|
||||
|
||||
private static boolean isDark(int color) {
|
||||
double brightness = (Color.red(color) * 299d
|
||||
+ Color.green(color) * 587d
|
||||
+ Color.blue(color) * 114d) / 1000d;
|
||||
return brightness < 128d;
|
||||
}
|
||||
|
||||
private static String hexOperandPattern() {
|
||||
return "#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})";
|
||||
}
|
||||
|
||||
private static final class LeadingColour {
|
||||
final int color;
|
||||
final int endIndex;
|
||||
|
||||
LeadingColour(int color, int endIndex) {
|
||||
this.color = color;
|
||||
this.endIndex = endIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Operand {
|
||||
final double[] rgb;
|
||||
final double alpha;
|
||||
final boolean hasAlpha;
|
||||
final boolean isHex;
|
||||
|
||||
Operand(double[] rgb, double alpha, boolean hasAlpha, boolean isHex) {
|
||||
this.rgb = rgb;
|
||||
this.alpha = alpha;
|
||||
this.hasAlpha = hasAlpha;
|
||||
this.isHex = isHex;
|
||||
private static String hexFor(int color) {
|
||||
if (Color.alpha(color) >= 255) {
|
||||
return String.format(java.util.Locale.ROOT, "#%06X", color & 0xFFFFFF);
|
||||
}
|
||||
return String.format(java.util.Locale.ROOT, "#%08X", color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@ package se.ajpanton.statusbartweak.shell.ui;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.UnderlineSpan;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
@@ -32,6 +36,7 @@ import com.google.android.material.switchmaterial.SwitchMaterial;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.shell.settings.BatteryBarGeometry;
|
||||
@@ -80,6 +85,8 @@ public final class BatteryBarFragment extends Fragment {
|
||||
TextView maxLevelLabel = root.findViewById(R.id.battery_bar_max_level_label);
|
||||
MaterialButton defaultDischargeButton = root.findViewById(R.id.battery_bar_default_discharge_button);
|
||||
MaterialButton defaultChargeButton = root.findViewById(R.id.battery_bar_default_charge_button);
|
||||
TextView colourHelpToggle = root.findViewById(R.id.battery_bar_colour_help_toggle);
|
||||
TextView colourHelp = root.findViewById(R.id.battery_bar_colour_help);
|
||||
MaterialButton addThresholdButton = root.findViewById(R.id.battery_bar_add_threshold_button);
|
||||
LinearLayout thresholdsContainer = root.findViewById(R.id.battery_bar_thresholds_container);
|
||||
View geometrySection = root.findViewById(R.id.battery_bar_geometry_section);
|
||||
@@ -145,6 +152,11 @@ public final class BatteryBarFragment extends Fragment {
|
||||
SbtSettings.KEY_BATTERY_BAR_DEFAULT_CHARGE_COLOR,
|
||||
SbtDefaults.BATTERY_BAR_DEFAULT_CHARGE_COLOR_DEFAULT),
|
||||
R.string.battery_bar_colour_same_as_default);
|
||||
bindExpandableHelp(
|
||||
colourHelpToggle,
|
||||
colourHelp,
|
||||
R.string.battery_bar_colour_help_show,
|
||||
R.string.battery_bar_colour_help_hide);
|
||||
|
||||
enabled.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
prefs.edit().putBoolean(SbtSettings.KEY_BATTERY_BAR_ENABLED, isChecked).apply();
|
||||
@@ -270,15 +282,129 @@ public final class BatteryBarFragment extends Fragment {
|
||||
return;
|
||||
}
|
||||
String normalised = BatteryBarStyle.normalizeColor(colour);
|
||||
String value;
|
||||
CharSequence value;
|
||||
int backgroundColor = Color.TRANSPARENT;
|
||||
boolean hasBackground = false;
|
||||
if (BatteryBarStyle.isThresholdChargeDefault(colour)) {
|
||||
value = getString(R.string.battery_bar_colour_same_as_default);
|
||||
} else if (normalised.isEmpty()) {
|
||||
value = getString(emptyLabelRes);
|
||||
} else {
|
||||
value = normalised;
|
||||
value = displayColourValue(normalised);
|
||||
backgroundColor = resolvePreviewColour(normalised);
|
||||
hasBackground = true;
|
||||
}
|
||||
button.setText(getString(labelRes) + ": " + value);
|
||||
SpannableStringBuilder label = new SpannableStringBuilder();
|
||||
label.append(compactColourLabel(labelRes));
|
||||
label.append(":\n");
|
||||
label.append(value);
|
||||
button.setText(label);
|
||||
if (!hasBackground) {
|
||||
button.setBackgroundTintList(null);
|
||||
button.setTextColor(ContextCompat.getColor(requireContext(), R.color.sbt_on_surface));
|
||||
} else {
|
||||
button.setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
|
||||
button.setTextColor(contrastTextColor(backgroundColor));
|
||||
}
|
||||
}
|
||||
|
||||
private String compactColourLabel(int labelRes) {
|
||||
String label = getString(labelRes).toLowerCase(Locale.ROOT);
|
||||
if (label.contains("discharge")) {
|
||||
return getString(R.string.battery_bar_button_label_discharge);
|
||||
}
|
||||
return getString(R.string.battery_bar_button_label_charge);
|
||||
}
|
||||
|
||||
private CharSequence displayColourValue(String expression) {
|
||||
String normalised = BatteryBarStyle.normalizeColor(expression);
|
||||
if (normalised.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
SpannableStringBuilder out = new SpannableStringBuilder();
|
||||
appendDisplayHex(out, resolveExpressionHex(normalised, Color.WHITE));
|
||||
if (hasTintVariant(normalised)) {
|
||||
out.append(" ; ");
|
||||
appendDisplayHex(out, resolveExpressionHex(normalised, Color.BLACK));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private void appendDisplayHex(SpannableStringBuilder out, String hex) {
|
||||
int start = out.length();
|
||||
out.append(hex);
|
||||
if (hex != null && hex.length() == 9) {
|
||||
out.setSpan(new UnderlineSpan(), start + 1, start + 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveExpressionHex(String expression, int referenceColor) {
|
||||
int fallback = Color.WHITE;
|
||||
int resolved = se.ajpanton.statusbartweak.shell.settings.ColourExpressionSupport.resolveColor(
|
||||
expression,
|
||||
referenceColor,
|
||||
fallback,
|
||||
true);
|
||||
if (Color.alpha(resolved) >= 255) {
|
||||
return String.format(Locale.ROOT, "#%06X", resolved & 0xFFFFFF);
|
||||
}
|
||||
return String.format(Locale.ROOT, "#%08X", resolved);
|
||||
}
|
||||
|
||||
private boolean hasTintVariant(String expression) {
|
||||
if (expression == null) {
|
||||
return false;
|
||||
}
|
||||
boolean inVector = false;
|
||||
for (int i = 0; i < expression.length(); i++) {
|
||||
char c = expression.charAt(i);
|
||||
if (inVector) {
|
||||
if (c == ']') {
|
||||
inVector = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == '[') {
|
||||
inVector = true;
|
||||
continue;
|
||||
}
|
||||
if (c == ';' || c == '|') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int resolvePreviewColour(String expression) {
|
||||
return se.ajpanton.statusbartweak.shell.settings.ColourExpressionSupport.resolveColor(
|
||||
expression,
|
||||
ContextCompat.getColor(requireContext(), R.color.sbt_on_surface),
|
||||
Color.WHITE,
|
||||
true);
|
||||
}
|
||||
|
||||
private int contrastTextColor(int backgroundColor) {
|
||||
int alpha = Color.alpha(backgroundColor);
|
||||
int red = compositeChannel(Color.red(backgroundColor), alpha, 255);
|
||||
int green = compositeChannel(Color.green(backgroundColor), alpha, 255);
|
||||
int blue = compositeChannel(Color.blue(backgroundColor), alpha, 255);
|
||||
double luminance = (0.299d * red + 0.587d * green + 0.114d * blue) / 255d;
|
||||
return luminance > 0.55d ? Color.BLACK : Color.WHITE;
|
||||
}
|
||||
|
||||
private int compositeChannel(int source, int alpha, int destination) {
|
||||
return Math.round((source * alpha + destination * (255 - alpha)) / 255f);
|
||||
}
|
||||
|
||||
private void bindExpandableHelp(TextView toggle, TextView content, int showRes, int hideRes) {
|
||||
if (toggle == null || content == null) {
|
||||
return;
|
||||
}
|
||||
toggle.setOnClickListener(v -> {
|
||||
boolean show = content.getVisibility() != View.VISIBLE;
|
||||
content.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||
toggle.setText(show ? hideRes : showRes);
|
||||
});
|
||||
}
|
||||
|
||||
private void trimMappingStepButton(MaterialButton button) {
|
||||
|
||||
@@ -570,6 +570,25 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/battery_bar_default_charge_colour" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/battery_bar_colour_help_toggle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/battery_bar_colour_help_show"
|
||||
android:textAppearance="?attr/textAppearanceBody1"
|
||||
android:textColor="@color/sbt_accent_dark"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/battery_bar_colour_help"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="gone"
|
||||
android:text="@string/battery_bar_colour_help_text"
|
||||
android:textAppearance="?attr/textAppearanceBody2" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
|
||||
@@ -165,11 +165,13 @@
|
||||
<string name="battery_bar_colours_title">Default colours</string>
|
||||
<string name="battery_bar_default_discharge_colour">Discharge colour</string>
|
||||
<string name="battery_bar_default_charge_colour">Charge colour</string>
|
||||
<string name="battery_bar_button_label_discharge">Discharge</string>
|
||||
<string name="battery_bar_button_label_charge">Charge</string>
|
||||
<string name="battery_bar_thresholds_title">Threshold colours</string>
|
||||
<string name="battery_bar_thresholds_hint">Below a threshold, the first matching colour overrides the default.</string>
|
||||
<string name="battery_bar_add_threshold">Add threshold</string>
|
||||
<string name="battery_bar_colour_dialog_title">Set colour</string>
|
||||
<string name="battery_bar_colour_dialog_hint">Examples: FA0, 80FA, or FFAA00</string>
|
||||
<string name="battery_bar_colour_dialog_hint">Examples: #FA0, FA0, or #FA0 ; invRGB 0.5 *RGB</string>
|
||||
<string name="battery_bar_threshold_percent_title">Threshold percent</string>
|
||||
<string name="battery_bar_threshold_percent_hint">Applies when battery is at or below this percent.</string>
|
||||
<string name="battery_bar_threshold_change_percent">Change threshold %</string>
|
||||
@@ -179,7 +181,10 @@
|
||||
<string name="battery_bar_threshold_remove">Remove threshold</string>
|
||||
<string name="battery_bar_colour_auto">Auto</string>
|
||||
<string name="battery_bar_colour_same_as_discharge">Same as discharge</string>
|
||||
<string name="battery_bar_colour_same_as_default">Same as default</string>
|
||||
<string name="battery_bar_colour_same_as_default">Default</string>
|
||||
<string name="battery_bar_colour_help_show">Show colour syntax</string>
|
||||
<string name="battery_bar_colour_help_hide">Hide colour syntax</string>
|
||||
<string name="battery_bar_colour_help_text">Colour values:\n#FA0, #FFAA00, #80FFAA00, FA0, or [255, 255, 170, 0]\n\nPostfix math:\n#FA0 1.2 *RGB clip\n#FA0 invRGB\n\nOperators:\n+ - * / ^ inv min max sqrt clip dup\n\nChannels:\nAdd A, R, G, or B to limit vector operations.\nExamples: invRGB, *A, +RB\n\nTint variants:\nUse ; for a dark-tint expression.\nThe dark expression starts with the result from the bright expression.</string>
|
||||
<string name="battery_bar_threshold_duplicate">A threshold at that percentage already exists.</string>
|
||||
<string name="battery_bar_threshold_reordered">That threshold moved to a new position in the list.</string>
|
||||
<string name="battery_bar_threshold_charge_note">Leave the field empty and press OK for \"Same as discharge\", or use the button below for \"Same as default\".</string>
|
||||
|
||||
+34
@@ -49,4 +49,38 @@ public final class ClockColorResolverTest {
|
||||
public void rejectsMoreThanTwoColorVariants() {
|
||||
resolver.resolveColor(null, "#123 ; #456 ; #789", WHITE_REFERENCE, 0xffffffff);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void batteryBarOptionsAllowBareHexColours() {
|
||||
assertEquals(
|
||||
0xff808080,
|
||||
resolver.resolveColor(
|
||||
null,
|
||||
"808080",
|
||||
WHITE_REFERENCE,
|
||||
0xffffffff,
|
||||
ClockColorResolver.Options.batteryBar()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void batteryBarOptionsKeepThreeDigitNumbersAsNumbers() {
|
||||
assertEquals(
|
||||
0xffffffff,
|
||||
resolver.resolveColor(
|
||||
null,
|
||||
"255",
|
||||
WHITE_REFERENCE,
|
||||
0xffffffff,
|
||||
ClockColorResolver.Options.batteryBar()));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void batteryBarOptionsRejectBatteryBarOperand() {
|
||||
resolver.resolveColor(
|
||||
null,
|
||||
"#batterybar",
|
||||
WHITE_REFERENCE,
|
||||
0xffffffff,
|
||||
ClockColorResolver.Options.batteryBar());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user