Add battery bar colour expressions

This commit is contained in:
ajp_anton
2026-06-18 14:56:01 +00:00
parent 7144b130f7
commit e1dfe637f8
6 changed files with 329 additions and 361 deletions
@@ -7,40 +7,66 @@ import java.util.Locale;
import se.ajpanton.statusbartweak.shell.settings.BatteryBarStyle; 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_ALPHA = 0;
private static final int CHANNEL_RED = 1; private static final int CHANNEL_RED = 1;
private static final int CHANNEL_GREEN = 2; private static final int CHANNEL_GREEN = 2;
private static final int CHANNEL_BLUE = 3; private static final int CHANNEL_BLUE = 3;
private static final int CHANNEL_COUNT = 4; private static final int CHANNEL_COUNT = 4;
String resolveColorHex( public String resolveColorHex(
Context context, Context context,
String expression, String expression,
int referenceColor, int referenceColor,
int fallbackColor int fallbackColor
) { ) {
int resolved = resolveColor(context, expression, referenceColor, 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) { if (alpha(resolved) >= 255) {
return String.format(Locale.ROOT, "#%06X", resolved & 0xFFFFFF); return String.format(Locale.ROOT, "#%06X", resolved & 0xFFFFFF);
} }
return String.format(Locale.ROOT, "#%08X", resolved); return String.format(Locale.ROOT, "#%08X", resolved);
} }
int resolveColor( public int resolveColor(
Context context, Context context,
String expression, String expression,
int referenceColor, int referenceColor,
int fallbackColor 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)) { if (isEmpty(expression)) {
return fallbackColor; return fallbackColor;
} }
Options resolvedOptions = options != null ? options : Options.clock();
VariantExpressions variants = splitVariants(expression); 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() Value dark = variants.darkExpression.isEmpty()
? bright ? bright
: evaluateExpression(context, variants.darkExpression, bright, fallbackColor); : evaluateExpression(context, variants.darkExpression, bright, fallbackColor, resolvedOptions);
return toColor(isDark(referenceColor) ? dark : bright); return toColor(isDark(referenceColor) ? dark : bright);
} }
@@ -48,7 +74,8 @@ final class ClockColorResolver {
Context context, Context context,
String expression, String expression,
Value inherited, Value inherited,
int fallbackColor int fallbackColor,
Options options
) { ) {
ArrayList<String> tokens = tokenize(expression); ArrayList<String> tokens = tokenize(expression);
ArrayList<Value> stack = new ArrayList<>(); ArrayList<Value> stack = new ArrayList<>();
@@ -56,7 +83,7 @@ final class ClockColorResolver {
stack.add(inherited.copy()); stack.add(inherited.copy());
} }
for (String token : tokens) { for (String token : tokens) {
applyToken(context, stack, token, fallbackColor); applyToken(context, stack, token, fallbackColor, options);
} }
if (stack.isEmpty()) { if (stack.isEmpty()) {
throw new IllegalArgumentException("Empty colour expression"); throw new IllegalArgumentException("Empty colour expression");
@@ -64,7 +91,13 @@ final class ClockColorResolver {
return top(stack).copy(); 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)) { if (isEmpty(token)) {
return; return;
} }
@@ -73,7 +106,7 @@ final class ClockColorResolver {
applyOperation(stack, operation); applyOperation(stack, operation);
return; return;
} }
stack.add(parseOperand(context, token, fallbackColor)); stack.add(parseOperand(context, token, fallbackColor, options));
} }
private Operation parseOperation(String token) { private Operation parseOperation(String token) {
@@ -244,12 +277,15 @@ final class ClockColorResolver {
stack.add(Value.vector(result)); 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() : ""; String value = token != null ? token.trim() : "";
if (value.isEmpty()) { if (value.isEmpty()) {
throw new IllegalArgumentException("Empty colour token"); throw new IllegalArgumentException("Empty colour token");
} }
if ("#batterybar".equalsIgnoreCase(value)) { 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))); return Value.vector(vectorForColor(BatteryBarStyle.resolveCurrentColor(context, fallbackColor, fallbackColor)));
} }
if (value.charAt(0) == '#') { if (value.charAt(0) == '#') {
@@ -258,6 +294,9 @@ final class ClockColorResolver {
if (value.charAt(0) == '[') { if (value.charAt(0) == '[') {
return Value.vector(parseVector(value)); return Value.vector(parseVector(value));
} }
if (options != null && options.allowBareHexOperand && looksLikeBareHex(value)) {
return Value.vector(vectorForColor(parseHexColor("#" + value)));
}
try { try {
return Value.number(Double.parseDouble(value)); return Value.number(Double.parseDouble(value));
} catch (NumberFormatException e) { } 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) { private double[] parseVector(String token) {
String value = token != null ? token.trim() : ""; String value = token != null ? token.trim() : "";
if (!value.startsWith("[") || !value.endsWith("]")) { if (!value.startsWith("[") || !value.endsWith("]")) {
@@ -608,4 +663,22 @@ final class ClockColorResolver {
return Math.max(0d, Math.min(255d, value)); 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);
}
}
} }
@@ -1,394 +1,105 @@
package se.ajpanton.statusbartweak.shell.settings; package se.ajpanton.statusbartweak.shell.settings;
import android.graphics.Color; import android.graphics.Color;
import android.text.TextUtils;
import java.util.Locale; import se.ajpanton.statusbartweak.runtime.features.clock.ClockColorResolver;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class ColourExpressionSupport { public final class ColourExpressionSupport {
private static final Pattern OPERATION_PATTERN = private static final ClockColorResolver RESOLVER = new ClockColorResolver();
Pattern.compile("([+\\-*/])\\s*(" + hexOperandPattern() + "|\\d+(?:\\.\\d+)?|\\.\\d+)"); private static final ClockColorResolver.Options OPTIONS = ClockColorResolver.Options.batteryBar();
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 ColourExpressionSupport() { private ColourExpressionSupport() {
} }
public static String normaliseExpression(String raw, boolean assumeHashPrefix) { public static String normaliseExpression(String raw, boolean assumeHashPrefix) {
if (raw == null) { String value = normaliseSeparators(raw);
return "";
}
String value = raw.trim();
if (value.isEmpty()) { if (value.isEmpty()) {
return ""; return "";
} }
int variantSplit = findVariantSplit(value); try {
String left = value; RESOLVER.resolveColor(null, value, Color.WHITE, Color.WHITE, OPTIONS);
String right = null; RESOLVER.resolveColor(null, value, Color.BLACK, Color.WHITE, OPTIONS);
if (variantSplit >= 0) { return value;
left = value.substring(0, variantSplit).trim(); } catch (IllegalArgumentException ignored) {
right = value.substring(variantSplit + 1).trim();
}
if (!isValidSegment(left, assumeHashPrefix, false, Color.WHITE)) {
return ""; 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, public static int resolveColor(
int referenceColor, String raw,
int fallbackColor, int referenceColor,
boolean assumeHashPrefix) { int fallbackColor,
String value = normaliseExpression(raw, assumeHashPrefix); boolean assumeHashPrefix
) {
String value = normaliseSeparators(raw);
if (value.isEmpty()) { if (value.isEmpty()) {
return fallbackColor; 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 { try {
resolveSegment(segment, baseColor, assumeHashPrefix, allowInheritedBase); return RESOLVER.resolveColor(null, value, referenceColor, fallbackColor, OPTIONS);
return true;
} catch (IllegalArgumentException ignored) { } catch (IllegalArgumentException ignored) {
return false; return fallbackColor;
} }
} }
private static int resolveSegment(String segment, public static String resolveColorHex(
int baseColor, String raw,
boolean assumeHashPrefix, int referenceColor,
boolean allowInheritedBase) { int fallbackColor,
String value = segment != null ? segment.trim() : ""; boolean assumeHashPrefix
) {
String value = normaliseSeparators(raw);
if (value.isEmpty()) { if (value.isEmpty()) {
throw new IllegalArgumentException("Empty colour segment"); return hexFor(fallbackColor);
}
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");
} }
try { try {
return Color.parseColor("#" + hex); return RESOLVER.resolveColorHex(null, value, referenceColor, fallbackColor, OPTIONS);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException ignored) {
throw new IllegalArgumentException("Invalid colour", e); return hexFor(fallbackColor);
} }
} }
private static String expandShortHex(String hex) { private static String normaliseSeparators(String raw) {
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) {
String value = raw != null ? raw.trim() : ""; String value = raw != null ? raw.trim() : "";
if (value.isEmpty() || value.charAt(0) != '#') { if (value.isEmpty()) {
throw new IllegalArgumentException("Expected colour literal"); return "";
} }
String hex = value.substring(1); int split = findTopLevelPipe(value);
if (hex.length() == 3 || hex.length() == 4) { if (split < 0) {
hex = expandShortHex(hex); return value;
} else if (hex.length() != 6 && hex.length() != 8) {
throw new IllegalArgumentException("Unsupported colour length");
} }
boolean hasAlpha = hex.length() == 8; return value.substring(0, split).trim() + " ; " + value.substring(split + 1).trim();
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);
} }
private static double parseHexByte(String value) { private static int findTopLevelPipe(String value) {
return Integer.parseInt(value, 16); boolean inVector = false;
} int split = -1;
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;
for (int i = 0; i < value.length(); i++) { for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i); char c = value.charAt(i);
if (c == '\'') { if (inVector) {
quoted = !quoted; if (c == ']') {
inVector = false;
}
continue; continue;
} }
if (!quoted && c == '|') { if (c == '[') {
return i; inVector = true;
continue;
}
if (c == '|') {
if (split >= 0) {
return -1;
}
split = i;
} }
} }
return -1; return split;
} }
private static boolean isDark(int color) { private static String hexFor(int color) {
double brightness = (Color.red(color) * 299d if (Color.alpha(color) >= 255) {
+ Color.green(color) * 587d return String.format(java.util.Locale.ROOT, "#%06X", color & 0xFFFFFF);
+ 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;
} }
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.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.UnderlineSpan;
import android.text.Editable; import android.text.Editable;
import android.text.InputType; import android.text.InputType;
import android.text.TextWatcher; import android.text.TextWatcher;
@@ -32,6 +36,7 @@ import com.google.android.material.switchmaterial.SwitchMaterial;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import se.ajpanton.statusbartweak.R; import se.ajpanton.statusbartweak.R;
import se.ajpanton.statusbartweak.shell.settings.BatteryBarGeometry; 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); TextView maxLevelLabel = root.findViewById(R.id.battery_bar_max_level_label);
MaterialButton defaultDischargeButton = root.findViewById(R.id.battery_bar_default_discharge_button); MaterialButton defaultDischargeButton = root.findViewById(R.id.battery_bar_default_discharge_button);
MaterialButton defaultChargeButton = root.findViewById(R.id.battery_bar_default_charge_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); MaterialButton addThresholdButton = root.findViewById(R.id.battery_bar_add_threshold_button);
LinearLayout thresholdsContainer = root.findViewById(R.id.battery_bar_thresholds_container); LinearLayout thresholdsContainer = root.findViewById(R.id.battery_bar_thresholds_container);
View geometrySection = root.findViewById(R.id.battery_bar_geometry_section); 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, SbtSettings.KEY_BATTERY_BAR_DEFAULT_CHARGE_COLOR,
SbtDefaults.BATTERY_BAR_DEFAULT_CHARGE_COLOR_DEFAULT), SbtDefaults.BATTERY_BAR_DEFAULT_CHARGE_COLOR_DEFAULT),
R.string.battery_bar_colour_same_as_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) -> { enabled.setOnCheckedChangeListener((buttonView, isChecked) -> {
prefs.edit().putBoolean(SbtSettings.KEY_BATTERY_BAR_ENABLED, isChecked).apply(); prefs.edit().putBoolean(SbtSettings.KEY_BATTERY_BAR_ENABLED, isChecked).apply();
@@ -270,15 +282,129 @@ public final class BatteryBarFragment extends Fragment {
return; return;
} }
String normalised = BatteryBarStyle.normalizeColor(colour); String normalised = BatteryBarStyle.normalizeColor(colour);
String value; CharSequence value;
int backgroundColor = Color.TRANSPARENT;
boolean hasBackground = false;
if (BatteryBarStyle.isThresholdChargeDefault(colour)) { if (BatteryBarStyle.isThresholdChargeDefault(colour)) {
value = getString(R.string.battery_bar_colour_same_as_default); value = getString(R.string.battery_bar_colour_same_as_default);
} else if (normalised.isEmpty()) { } else if (normalised.isEmpty()) {
value = getString(emptyLabelRes); value = getString(emptyLabelRes);
} else { } 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) { private void trimMappingStepButton(MaterialButton button) {
@@ -570,6 +570,25 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text="@string/battery_bar_default_charge_colour" /> 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> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
+7 -2
View File
@@ -165,11 +165,13 @@
<string name="battery_bar_colours_title">Default colours</string> <string name="battery_bar_colours_title">Default colours</string>
<string name="battery_bar_default_discharge_colour">Discharge colour</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_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_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_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_add_threshold">Add threshold</string>
<string name="battery_bar_colour_dialog_title">Set colour</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_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_percent_hint">Applies when battery is at or below this percent.</string>
<string name="battery_bar_threshold_change_percent">Change threshold %</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_threshold_remove">Remove threshold</string>
<string name="battery_bar_colour_auto">Auto</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_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_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_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> <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>
@@ -49,4 +49,38 @@ public final class ClockColorResolverTest {
public void rejectsMoreThanTwoColorVariants() { public void rejectsMoreThanTwoColorVariants() {
resolver.resolveColor(null, "#123 ; #456 ; #789", WHITE_REFERENCE, 0xffffffff); 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());
}
} }