diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockColorResolver.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockColorResolver.java index 76c16b1..85ab262 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockColorResolver.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockColorResolver.java @@ -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 tokens = tokenize(expression); ArrayList 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 stack, String token, int fallbackColor) { + private void applyToken( + Context context, + ArrayList 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); + } + } } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/ColourExpressionSupport.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/ColourExpressionSupport.java index b07f420..4ff266c 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/ColourExpressionSupport.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/ColourExpressionSupport.java @@ -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); } } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/BatteryBarFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/BatteryBarFragment.java index 2117b23..fe946fe 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/BatteryBarFragment.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/BatteryBarFragment.java @@ -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) { diff --git a/app/src/main/res/layout/fragment_battery_bar.xml b/app/src/main/res/layout/fragment_battery_bar.xml index 897b8ae..0893075 100644 --- a/app/src/main/res/layout/fragment_battery_bar.xml +++ b/app/src/main/res/layout/fragment_battery_bar.xml @@ -570,6 +570,25 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/battery_bar_default_charge_colour" /> + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 785f928..72bb53e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -165,11 +165,13 @@ Default colours Discharge colour Charge colour + Discharge + Charge Threshold colours Below a threshold, the first matching colour overrides the default. Add threshold Set colour - Examples: FA0, 80FA, or FFAA00 + Examples: #FA0, FA0, or #FA0 ; invRGB 0.5 *RGB Threshold percent Applies when battery is at or below this percent. Change threshold % @@ -179,7 +181,10 @@ Remove threshold Auto Same as discharge - Same as default + Default + Show colour syntax + Hide colour syntax + 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. A threshold at that percentage already exists. That threshold moved to a new position in the list. Leave the field empty and press OK for \"Same as discharge\", or use the button below for \"Same as default\". diff --git a/app/src/test/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockColorResolverTest.java b/app/src/test/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockColorResolverTest.java index 17ea3bd..cdd12c0 100644 --- a/app/src/test/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockColorResolverTest.java +++ b/app/src/test/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockColorResolverTest.java @@ -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()); + } }