diff --git a/app/src/main/java/io/mrarm/irc/util/IRCColorUtils.java b/app/src/main/java/io/mrarm/irc/util/IRCColorUtils.java index cbd15272..e9cec223 100644 --- a/app/src/main/java/io/mrarm/irc/util/IRCColorUtils.java +++ b/app/src/main/java/io/mrarm/irc/util/IRCColorUtils.java @@ -19,25 +19,22 @@ import io.mrarm.irc.R; /* - * NOTE: colours are referred to in several different ways: - * 1. "Color": a 32-bit value comprising 4 8-bit values, A,R,G,B. This is the - * native Android "Color" type. - * 2. "Color ID": an opaque value that we can retrieve a value from the - * runtime configuration. (Essentially it's a protobuf index number that's + * NOTE: colors are referred to in several different ways: + * 1. A native Android "Color": a 32-bit integer intepreted as 4 8-bit + * channels (Alpha, Red, Green, & Blue). + * 2. "ColorId": an opaque key whose value we can retrieve from the runtime + * configuration. (Essentially it's a protobuf field index number that's * assigned when compiling the attr.xml resources file.) - * 3. "Color Code": a 32-bit int that decomposes into 8 tag bits (bits 31-24) - * and a 24-bit value (bits 23-0). The interpretation of the 'value' then - * depends on the tag bits: which are checked in this order: - * → bit 27 means use the default color (and the value and the other - * tag bits are ignored when the color code is interpreted, but should - * be set to mIRC color index 99 with both bits 27 and 24 set) - * → bit 26 means the value is an ANSI color index between 0 and 255; - * → bit 25 means the value is an mIRC color index between 0 and 98; - * → bit 24 means the value is an RGB value (with A is fixed as 255); - * → if all bits 24-31 are 0, treat the value is a color-ID (as above); - * ← any other value is illegal and will raise an exception - * The "fg" and "bg" variables hold "tagged color codes", so that the logic - * that sets them doesn't have to do mapping between colour spaces. + * 3. "ColorCode" or "tagged Color Code": a 32-bit int that comprises an + * 8-bit tag, and a 24-bit value. The tag determines the interpretation of + * the value. + * + * COLOR_ARGB_FLAG is chosen to be the bottom bit of Alpha so that setting it + * within an ARGB value should be imperceptible, but just to be sure, treat + * fully transparent as a separate case. + * + * COLOR_SPACE_ID is chosen to be 0 so that ColorId values are directly usable + * as ColorCode values. */ public class IRCColorUtils { @@ -49,21 +46,104 @@ public class IRCColorUtils { public static final int COLOR_MEMBER_VOICE = R.styleable.IRCColors_colorMemberVoice; public static final int COLOR_MEMBER_NORMAL = R.styleable.IRCColors_colorMemberNormal; - private static final int COLOR_SPACE_24BIT = 0x1000000; - private static final int COLOR_SPACE_MIRC = 0x2000000; - private static final int COLOR_SPACE_ANSI = 0x4000000; - private static final int COLOR_SPACE_DEFAULT = 0x8000000; - private static final int COLOR_DEFAULT = COLOR_SPACE_DEFAULT|COLOR_SPACE_MIRC|99; + private static final int COLOR_ARGB_FLAG = 0x1000000; // @ColorInt + private static final int COLOR_SPACE_ID = 0x0000000; // @ColorInt + private static final int COLOR_SPACE_MIRC = 0x2000000; // @ColorInt + private static final int COLOR_SPACE_ANSI = 0x4000000; // @ColorInt + private static final int COLOR_INVISIBLE = 0x6000000; // @ColorInt + private static final int COLOR_DEFAULT = 0x8000000; // @ColorInt - private static class BaseColorException extends RuntimeException {}; + private static final Color COLOR_FAULT = Color.RED; // @ColorInt + + private static class BaseColorException extends RuntimeException { + BaseColorException(String m, int c) { super(m + " " + Integer.toHexString(c)); } + BaseColorException(String m) { super(m); } + BaseColorException(int c) { super(Integer.toHexString(c)); } + }; private static class InvalidColorCodeException extends BaseColorException { - InvalidColorCodeException(int c) { super("Invalid color code "+Integer.toHexString(c)); } + InvalidColorCodeException(int c) { super("Invalid color code", c); } + }; + private static class InvalidColorIdException extends BaseColorException { + InvalidColorIdException(int c) { super("Invalid color ID ", c); } }; private static class MissingThemeColorException extends BaseColorException { MissingThemeColorException(String m) { super(m); } - MissingThemeColorException() { super(); } + MissingThemeColorException() { super("Missing Theme or Element"); } }; + /*========================================================================== + * + * Map "ColorId" values to (tagged) "ColorCode" values. + * + * This will be a direct ARGB code (with the COLOR_ARGB_FLAG bit set), + * except for two cases: + * 1. If the configuration does not contain a specification for a given + * ColorId, then COLOR_DEFAULT is substituted. + * 2. If the specified ARGB value has a 0 alpha value (fully transparent) + * then COLOR_INVISIBLE is substituted; this is done because setting + * COLOR_ARGB_FLAG will prevent the alpha value being 0. + */ + + private static int[] colorIdMap = null; + + /* loadColorIdMap makes a fast cached version of the ColorId to ColorCode + * mappings in the Styling resources; */ + public static void loadColorIdMap(Resources.Theme theme, int resId) { + TypedArray ta = theme.obtainStyledAttributes(resId, R.styleable.IRCColors); + colorIdMap = new int[R.styleable.IRCColors.length]; + for (int i = 0; i < colorIdMap.length; i++) { + int c = COLOR_DEFAULT; + try { + int j = i; + TypedValue tv; + while ((tv = ta.peekValue(j)) != null && tv.type == TypedValue.TYPE_ATTRIBUTE) + j = Arrays.binarySearch(R.styleable.IRCColors, tv.data); + c = ta.getColor(j, COLOR_FAULT); + } catch (UnsupportedOperationException e) + e.printStackTrace(); + + if ( ( c & 0xff000000 ) == 0 ) + c = COLOR_INVISIBLE; + else + c |= COLOR_ARGB_FLAG; + colorIdMap[i] = c; + } + ta.recycle(); + invalidateColorMaps(); + } + + public static int getColorById(Context context, int colorId) { + if (colorIdMap == null) + loadColorIdMap(context.getTheme(), R.style.AppTheme_IRCColors); + if (colorId < 0 || colorId >= colorIdMap.length) + throw new InvalidColorIdException(colorId); + return colorIdMap[colorId]; + } + + /*========================================================================== + * + * Map "ColorCode" values to native ARGB Color integers. + * + * If the low-order bit of the tag is 1, then the entire code is taken + * as an ARGB value (with the tag used as the alpha value); otherwise the + * other bits of the tag select: + * * 00 a code ID, as above + * * 02 an mIRC color code (000000 to 000063) + * * 04 an ANSI color code (000000 to 0000ff) + * * 06 invisible (completely transparent) + * * 08 default (which differs for foreground and background) + * Other values will raise an InvalidColorCodeException, so that the entire + * IRC line will be rendered without any interpretation of control codes. + * + * We maintain two color maps: mIRC colors, and ANSI colors. + * + * The mIRC colors are divided into 16 configurable colors and 73 fixed colors. + * The ANSI colors are similarly divided into 16 configurable colors, a 6×6×6 + * color-cube, and a 24-step gray-scale. In principle the color cube and grayscale + * are computable, based on a selected γ, but currently they're simply fixed, based + * on the color palette used by XTerm. + */ + private static int[] MIRC_COLOR_MAP_TEMPLATE = new int[] { R.styleable.IRCColors_colorWhite, R.styleable.IRCColors_colorBlack, @@ -109,8 +189,6 @@ private static class MissingThemeColorException extends BaseColorException { 0xff9f9f9f, 0xffbcbcbc, 0xffe2e2e2, 0xffffffff }; - private static int[] MIRC_COLOR_MAP = null; - private static int[] ANSI_COLOR_MAP_TEMPLATE = new int[] { R.styleable.IRCColors_colorBlack, R.styleable.IRCColors_colorRed, /* only in ANSI color-space */ @@ -180,89 +258,103 @@ private static class MissingThemeColorException extends BaseColorException { 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee }; + private static int[] MIRC_COLOR_MAP = null; private static int[] ANSI_COLOR_MAP = null; - private static int colorMapResId = R.style.AppTheme_IRCColors; - - private static int[] NICK_COLORS = new int[] { 3, 4, 7, 8, 9, 10, 11, 12, 13 }; - - //private static int[] sColorValues = null; - - /* loadColorIdMap makes a fast cached version of the ColorId to Color - * mappings in the Styling resources; */ - public static void loadColorIdMap(Resources.Theme theme, int resId) { - TypedArray ta = theme.obtainStyledAttributes(resId, R.styleable.IRCColors); - sColorValues = new int[R.styleable.IRCColors.length]; - for (int i = 0; i < sColorValues.length; i++) { - try { - int j = i; - TypedValue tv; - while ((tv = ta.peekValue(j)) != null && tv.type == TypedValue.TYPE_ATTRIBUTE) - j = Arrays.binarySearch(R.styleable.IRCColors, tv.data); - sColorValues[i] = ta.getColor(j, Color.RED); - } catch (UnsupportedOperationException e) { - e.printStackTrace(); - sColorValues[i] = Color.RED; - } - } - ta.recycle(); - // invalidate the color code maps - MIRC_COLOR_MAP = null; - ANSI_COLOR_MAP = null; - } - - public static int getColorById(Context context, int colorId) { - if (sColorValues == null) - loadColorIdMap(context.getTheme(), R.style.AppTheme_IRCColors); - return sColorValues[colorId]; - } - private static int[] fillColorMapTemplate(Context context, int[] map_template) { int[] newmap = new int[map_template.length]; int i; for (i=0;i= MIRC_COLOR_MAP.length) + switch (colorCode >> 24) { /* >>24 ignores v */ + case COLOR_SPACE_MIRC >> 24: + if (MIRC_COLOR_MAP == null) + MIRC_COLOR_MAP = fillColorMapTemplate(context, MIRC_COLOR_MAP_TEMPLATE); + if (v >= MIRC_COLOR_MAP.length) + throw new InvalidColorCodeException(colorCode); + return MIRC_COLOR_MAP[v]; + case COLOR_SPACE_ANSI >> 24: + if (ANSI_COLOR_MAP == null) + ANSI_COLOR_MAP = fillColorMapTemplate(context, ANSI_COLOR_MAP_TEMPLATE); + if (v >= ANSI_COLOR_MAP.length) + throw new InvalidColorCodeException(colorCode); + return ANSI_COLOR_MAP[v]; + case COLOR_SPACE_ID >> 24: + return getColorById(context, v); + case COLOR_INVISIBLE >> 24: + return 0x00000000; // ARGB with A set to 0 + case COLOR_DEFAULT >> 24: /* never call this function with a "default" color */ throw new InvalidColorCodeException(colorCode); - return MIRC_COLOR_MAP[v]; - } - if ((f & COLOR_SPACE_ANSI) != 0) { - if (ANSI_COLOR_MAP == null) - ANSI_COLOR_MAP = fillColorMapTemplate(context, ANSI_COLOR_MAP_TEMPLATE); - if (v >= ANSI_COLOR_MAP.length) + default: throw new InvalidColorCodeException(colorCode); - return ANSI_COLOR_MAP[v]; } - try { - if (f==0) - return getColorById(context, v); - } catch(Exception e) {} - throw new InvalidColorCodeException(colorCode); } + public static int getIrcColor(Context context, int colorCode) { + return codeToArgb(context, colorCode | COLOR_SPACE_MIRC); + } + + private static int bestMatch(int [] colorMap, int color) { + int r = Color.red(color), + g = Color.green(color), + b = Color.blue(color); + int best = -1; + int bestDiff = 0x300; // any number larger than 3×255 + for (int i = 0; i < colorMap.length; i++) { + int c = colorMap[i]; + int diff = Math.abs(Color.red(c) - r) + + Math.abs(Color.green(c) - g) + + Math.abs(Color.blue(c) - b); + if (diff < bestDiff) { + bestDiff = diff; + best = i; + } + } + return best; + } + + /* Reverse this process to get a code in the given space */ + private static int argbToCode(Context context, int color, int space) { + switch (space >> 24) { + case COLOR_SPACE_MIRC >> 24: + if (MIRC_COLOR_MAP == null) + MIRC_COLOR_MAP = fillColorMapTemplate(context, MIRC_COLOR_MAP_TEMPLATE); + return bestMatch(MIRC_COLOR_MAP, color) | COLOR_SPACE_MIRC; + case COLOR_SPACE_ANSI >> 24: + if (ANSI_COLOR_MAP == null) + ANSI_COLOR_MAP = fillColorMapTemplate(context, ANSI_COLOR_MAP_TEMPLATE); + return bestMatch(ANSI_COLOR_MAP, color) | COLOR_SPACE_ANSI; + } + return color | COLOR_ARGB_FLAG; + } + + /*================================================================================ + * + * Various "preferred colors" + */ + public static int getStatusTextColor(Context context) { return getColorById(context, R.styleable.IRCColors_colorStatusText); } @@ -283,25 +375,23 @@ public static int getBanMaskColor(Context context) { return getColorById(context, R.styleable.IRCColors_colorLightRed); } + private static int[] NICK_COLORS = new int[] { + 3 | COLOR_SPACE_MIRC, + 4 | COLOR_SPACE_MIRC, + 7 | COLOR_SPACE_MIRC, + 8 | COLOR_SPACE_MIRC, + 9 | COLOR_SPACE_MIRC, + 10 | COLOR_SPACE_MIRC, + 11 | COLOR_SPACE_MIRC, + 12 | COLOR_SPACE_MIRC, + 13 | COLOR_SPACE_MIRC, + }; + public static int getNickColor(Context context, String nick) { int sum = 0; for (int i = 0; i < nick.length(); i++) sum += nick.charAt(i); - return getIrcColor(context, NICK_COLORS[sum % NICK_COLORS.length]); - } - - public static int findNearestIRCColor(Context context, int color) { - int ret = -1; - int retDiff = -1; - for (int i = 0; i < MIRC_COLOR_IDS.length; i++) { - int c = getIrcColor(context, i|COLOR_SPACE_MIRC); - int diff = Math.abs(Color.red(c) - Color.red(color)) + Math.abs(Color.green(c) - Color.green(color)) + Math.abs(Color.blue(c) - Color.blue(color)); - if (diff < retDiff || retDiff == -1) { - retDiff = diff; - ret = i; - } - } - return ret; + return codeToArgb(context, NICK_COLORS[sum % NICK_COLORS.length]); } public static CharSequence getFormattedString(Context context, String string) { @@ -309,42 +399,57 @@ public static CharSequence getFormattedString(Context context, String string) { ColoredTextBuilder builder = new ColoredTextBuilder(); appendFormattedString(context, builder, string); return builder.getSpannable(); - } catch (Exception e) { + } catch (Exception e) return new SpannableString(string); - } } private static bool isAsciiDigit(char c) { return c >= '0' && c <= '9'; } + private static class InvalidEscapeSequence extends RuntimeException { + public boolean rewind() { return true; } + InvalidEscapeSequence() { super(); rewind = true; } + }; + private static class AbortEscapeSequence extends InvalidEscapeSequence { + public boolean rewind() { return false; } + AbortEscapeSequence() { super(); } + }; + public static void appendFormattedString(Context context, ColoredTextBuilder builder, String string) { int fg = COLOR_DEFAULT, bg = COLOR_DEFAULT; - boolean ansi_inverse = false, + boolean ansi_hidden = false, + ansi_inverse = false, bold = false, italic = false, - underline = false; + underline = false, + strikeout = false; SpannableStringBuilder spannable = builder.getSpannable(); int len = string.length(); for (int i = 0; i < len; ) { - int ofg = fg, - obg = bg; + /* Previous effective fg & bg, as modified by previous ansi_hidden + * and ansi_inverse */ + int oeFg = fg, + oeBg = bg; if (ansi_inverse) { - ofg = bg; - obg = fg; + oeFg = bg; + oeBg = fg; } + if (ansi_hidden) + oeFg = oeBg; boolean oBold = bold, oItalic = italic, + oStrikeout = strikeout, oUnderline = underline; char c = string.charAt(i); i++; switch (c) { case 0x02: { // ^B, bold bold = !bold; - break; - } + } break; + case 0x03: { // ^C, color if (i < len && isAsciiDigit(c = string.charAt(i))) { i++; @@ -369,150 +474,201 @@ public static void appendFormattedString(Context context, ColoredTextBuilder bui } } else fg = bg = COLOR_DEFAULT; - break; - } + } break; + + case 0x05: { // ^E, strikethrough + strikeout = !strikeout; + } break; + case '\n': { // ^J, newline, \n spannable.append('\n'); fg = bg = COLOR_DEFAULT; bold = italic = underline = false; - break; - } + } break; + case 0x0F: { // ^O, reset - reset_all: fg = bg = COLOR_DEFAULT; - bold = italic = underline = false; - break; - } + ansi_hidden = ansi_inverse = + bold = italic = strikeout = underline = false; + } break; + case 0x16: { // ^W, swap bg and fg - fg = pBg; - bg = pFg; + int t = fg; + fg = bg; + bg = t; break; } case 0x1B: { // ^[, ESC, \e int oi = i; - if (i+1 < len && string.charAt(i) == '[') { + try { + if (i+1 >= len || string.charAt(i) != '[') + /* ESC not followed by [ */ + throw new InvalidEscapeSequence(); + /* We've seen an ANSI control sequence introducer (CSI): \e[ */ i++; - /* we have seen CSI (the start of an ANSI sequence, "\e[") */ /* in C we could write the following loop as just: * i += strspn(string+i, " !\"#$%&'()*+,-./0123456789:;<=>?"); */ - usable = true; + boolean usable = true; while (i < len && (c = string.charAt(i++)) >= 0x20 && c <= 0x3f) - if (!isAsciiDigit(c) && c != ';') + if (!isAsciiDigit(c) && c != ';' && c != ':') usable = false; - if (c >= 0x40 && c <= 0x7f) { - /* We have a syntactically valid ANSI escape sequence */ - if (c == 'm' && usable) { - /* Attribute setting ... */ - ArrayList params = new ArrayList(); - int x = 0; - for (int j=oi+1 ; j 0x7f) { + /* We've seen a sequence starting with CSI but + * stopped at an invalid byte, so treat it as any + * other invalid sequence. */ + throw new InvalidEscapeSequence(); + } + /* We've seen a syntactically valid ANSI escape + * sequence */ + if (c != 'm' || !usable) { + /* We've seen a valid but unknown sequence + * comprising CSI [\x20-\x3f]* [\x4-\x7f] + * which we can simply ignore. */ + continue; + } + + /* We've seen a valid Attribute Setting sequence, + * starting with CSI and ending with 'm' with only + * digits, colons and semicolons in between. + * + * Interpret this as a series of parameters are + * separated by semicolons, which in turn may be + * subdivided into subparameters separated by colons. + * + * Because colons and semicolons are sometimes treated + * interchangeably, we treat parameters and + * sub-parameters as a single flat array, but remember + * whether or not each one was preceded by a colon. */ + int estSize = (i-oi) / 3 + 3; + ArrayList params = new ArrayList<>(estSize); + ArrayList colons = new ArrayList<>(estSize); + { + colons.add(false); /* first param is not preceded by a colon */ + int x = 0; + for (int j=oi+1 ; j= params.length) throw new AbortEscapeSequence(); + int newColor = COLOR_DEFAULT; + boolean colon = colons.get(j); + switch (params.get(j)) { + case 2: { + j += colon ? 4 : 3; + if (j >= params.length) throw new AbortEscapeSequence(); + /* direct RGB specification + * ESC [ ... 38/48 ; 2 ; R ; G ; B ... m OR + * ESC [ ... 38/48 : 2 : : R : G : B ... m */ + newColor = Color.rgb(params.get(j-2), + params.get(j-1), + params.get(j)) + | COLOR_ARGB_FLAG; + } break; + + case 5: { + j++; + if (j >= params.length) throw new AbortEscapeSequence(); + if (j < params.length) { + /* ANSI color lookup + * ESC [ ... 38/48 ; 5 ; LOOKUP ... m OR + * ESC [ ... 38/48 : 5 : LOOKUP ... m + */ + newColor = params.get(j) | COLOR_SPACE_ANSI; } - /* ANSI colour with unknown colourspace */ - j = params.length; /* immediately stop */ - bg = COLOR_DEFAULT; - break; - } - case 49: bg = COLOR_DEFAULT; break; // background default - case 100: case 101: case 102: case 103: case 104: case 105: case 106: case 107: { - /* bright background */ - bg = p-100 | 8 | COLOR_SPACE_ANSI; - break; - } - + } break; + + default: { + if (!colon) + throw new AbortEscapeSequence(); + /* resume once past all colon-separated elements */ + for (;j+1 < params.length && colons.get(j+1); ++j) {} + } break; } - } - /* We've seen a valid attribute-setting escape - * sequence, so resume parsing from the the char - * after the 'm' */ - break; + /* ANSI color with unknown colorspace */ + switch (p) { + case 38: fg = newColor; break; + case 48: bg = newColor; break; + } + } break; + } + /* continue processing next attribute within the + * same CSI ... 'm' sequence */ } } - /* We've encountered a broken or invalid escape sequence */ - /* (a) display a "printable ESC symbol" U+241B */ - spannable.append((char) 0x241b); - /* (b) resume parsing immediately following the ESC character. */ - i = oi; - continue; + catch (InvalidEscapeSequence e) + if (e.rewind()) { + /* We've encountered a broken or invalid escape + * sequence, so + * (a) display a "printable ESC symbol" U+241B, and */ + spannable.append('␛'); + /* (b) resume parsing immediately following the ESC character. */ + i = oi; + continue; + } + /* We've seen a valid attribute-setting escape sequence, so + * resume parsing from the the char after the 'm' */ + break; } case 0x1D: { // ^], italic italic = !italic; @@ -531,28 +687,37 @@ public static void appendFormattedString(Context context, ColoredTextBuilder bui } } + /* The mIRC 'invert' code simply swaps the current foreground + * and background colors. + * In contrast, ANSI inverse is a flag that is either set or + * reset; when it changes, the foreground & background colors + * do swap, but that's not all: attempting to set the + * foreground color while ansi_inverse is 'on' will actually + * set the background color, and vice versa. + * + * Compute current effective fg & bg as modified by the ansi_hidden + * and ansi_inverse flags */ + int eFg = fg, + eBg = bg; if (ansi_inverse) { - /* The mIRC 'invert' code simply swaps the current foreground - * and background colours. - * In contrast, ANSI inverse is a flag that is either set or - * reset; when it changes, the foreground & background colours - * do swap, but that's not all: attempting to set the - * foreground colour while ansi_inverse is 'on' will actually - * set the background colour, and vice versa. */ - int t = bg; - bg = fg; - fg = t; + int t = eBg; + eBg = eFg; + eFg = t; } + if (ansi_hidden) + eFg = eBg; /* Skip if no changes */ - if ( fg == pFg - && bg == pBg + if ( eFg == oeFg + && eBg == oeBg && bold == oBold && italic == oItalic + && strikeout == oStrikeout && underline == oUnderline ) continue; - if (fg == COLOR_DEFAULT && bg == COLOR_DEFAULT && ! bold && ! italic && ! underline) { + if (eFg == COLOR_DEFAULT && eBg == COLOR_DEFAULT + && ! bold && ! italic && ! underline) { /* Quickly reset everything to defaults: by closing all spans */ builder.endSpans(Object.class); continue; @@ -579,103 +744,128 @@ public static void appendFormattedString(Context context, ColoredTextBuilder bui builder.endSpans(UnderlineSpan.class); } /* Change fg and/or bg */ - if (bg != pBg) { + if (eBg != oeBg) { /* Use this if spans have to nest properly */ - if (false && pFg != COLOR_DEFAULT) { + if (false && oeFg != COLOR_DEFAULT) { builder.endSpans(ForegroundColorSpan.class); - pFg = COLOR_DEFAULT; + oeFg = COLOR_DEFAULT; } /* end nesting enforcement */ - if (pBg != COLOR_DEFAULT) + if (oeBg != COLOR_DEFAULT) builder.endSpans(BackgroundColorSpan.class); - if (bg != COLOR_DEFAULT) - builder.setSpan(new BackgroundColorSpan(getIrcColor(context, bg))); + if (eBg != COLOR_DEFAULT) + builder.setSpan(new BackgroundColorSpan(codeToArgb(context, eBg))); } - if (fg != pFg) { - if (pFg != COLOR_DEFAULT) + if (eFg != oeFg) { + if (oeFg != COLOR_DEFAULT) builder.endSpans(ForegroundColorSpan.class); - if (fg != COLOR_DEFAULT) - builder.setSpan(new ForegroundColorSpan(getIrcColor(context, fg))); + if (eFg != COLOR_DEFAULT) + builder.setSpan(new ForegroundColorSpan(codeToArgb(context, eFg))); } } } + /* Currently we only output mIRC codes. + * TODO: 1) add option to send ANSI attribute codes & basic color codes; + * 2) add option to send ANSI 24-bit color codes. + */ public static String convertSpannableToIRCString(Context context, Spannable spannable) { - int n; -<<<<<<< HEAD - int fg = COLOR_DEFAULT; - int bg = COLOR_DEFAULT; - boolean bold = false; - boolean italic = false; - boolean underline = false; - StringBuilder ret = new StringBuilder(spannable.length()); - for (int i = 0; i < spannable.length(); i = n) { - n = spannable.nextSpanTransition(i, spannable.length(), Object.class); - int nFg = 99; - int nBg = 99; - boolean nBold = false; - boolean nItalic = false; - boolean nUnderline = false; -======= - int pFg = COLOR_DEFAULT; - int pBg = COLOR_DEFAULT; - boolean pBold = false; - boolean pItalic = false; - boolean pUnderline = false; + int fg = COLOR_DEFAULT, + bg = COLOR_DEFAULT; + boolean bold = false, + italic = false, + underline = false, + strikeout = false; StringBuilder ret = new StringBuilder(spannable.length()); - for (int i = 0; i < spannable.length(); i = n) { - n = spannable.nextSpanTransition(i, spannable.length(), Object.class); - int fg = COLOR_DEFAULT; - int bg = COLOR_DEFAULT; - boolean bold = false; - boolean italic = false; - boolean underline = false; ->>>>>>> 566ae32... Convert "colour ID" tables to colour map templates - for (Object span : spannable.getSpans(i, n, Object.class)) { + int nextSpan; + for (int i = 0; i < spannable.length(); i = nextSpan) { + nextSpan = spannable.nextSpanTransition(i, spannable.length(), Object.class); + /* These intentionally do NOT inherit from the "current" settings */ + int nFg = COLOR_DEFAULT, + nBg = COLOR_DEFAULT; + boolean nBold = false, + nItalic = false, + nUnderline = false, + nStrikeout = false; + for (Object span : spannable.getSpans(i, nextSpan, Object.class)) { int flags = spannable.getSpanFlags(span); if ((flags & Spannable.SPAN_COMPOSING) != 0) continue; - if (span instanceof ForegroundColorSpan) { - nFg = findNearestIRCColor(context, ((ForegroundColorSpan) span).getForegroundColor()); - } else if (span instanceof BackgroundColorSpan) { - nBg = findNearestIRCColor(context, ((BackgroundColorSpan) span).getBackgroundColor()); - } else if (span instanceof StyleSpan) { + if (span instanceof ForegroundColorSpan) + nFg = argbToCode(context, ((ForegroundColorSpan) span).getForegroundColor(), COLOR_SPACE_MIRC); + else if (span instanceof BackgroundColorSpan) + nBg = argbToCode(context, ((BackgroundColorSpan) span).getBackgroundColor(), COLOR_SPACE_MIRC); + else if (span instanceof StyleSpan) { int style = ((StyleSpan) span).getStyle(); - if (style == Typeface.BOLD || style == Typeface.BOLD_ITALIC) - nBold = true; - if (style == Typeface.ITALIC || style == Typeface.BOLD_ITALIC) - nItalic = true; - } else if (span instanceof UnderlineSpan) { + /* make use of sensible bitwise numbering */ + nBold = (style & Typeface.BOLD ) != 0; + nItalic = (style & Typeface.ITALIC) != 0; + } else if (span instanceof UnderlineSpan) nUnderline = true; - } + else if (span instanceof StrikethroughSpan) + nStrikeout = true; } - if ((!nBold && bold) || (!nItalic && italic) || (!nUnderline && underline)) { + if ( !( nBold || nItalic || nUnderline || nFg != COLOR_DEFAULT || nBg != COLOR_DEFAULT ) + && ( bold || italic || underline || fg != COLOR_DEFAULT || bg != COLOR_DEFAULT )) { ret.append((char) 0x0F); - fg = -1; - bg = -1; + fg = COLOR_DEFAULT; + bg = COLOR_DEFAULT; bold = false; italic = false; underline = false; } - if (nBold && !bold) + if (nFg != fg || nBg != bg) { + /* Check whether the char following this sequence could be + * miscontrued as part of the sequence itself. + * + * In particular, digits and commas. + * For the sake of other (possibly broken) IRC clients, and + * even though it's not technically necessary, treat a + * following comma as a "digit", so that we generate a full + * color code sequence, */ + boolean followedByCChar = (isAsciiDigit(spannable.chatAt(i)) + || ',' == spannable.chatAt(i)) + && nBold == bold + && nItalic == italic + && nUnderline == underline + && nStrikeout == strikeout; + int xBg = nBg == COLOR_DEFAULT + || nBg == COLOR_INVISIBLE ? 99 + : nBg % 100; + int xFg = nFg == COLOR_DEFAULT ? 99 : + nBg == COLOR_INVISIBLE ? xBg : + nFg % 100; + ret.append((char) 0x03); + /* omit «99,99» color pair if we're sure there's a non-digit following */ + if (xBg != 99 || xBg != 99 || followedByCChar) { + ret.append(xFg); + ret.append(','); + /* Pad to 2 digits unless we're sure there's a non-digit following */ + if (xBg < 10 && followedByCChar) + ret.append('0'); + ret.append(xBg); + } + fg = nFg; + bg = nBg; + } + if (nBold != bold) { ret.append((char) 0x02); - if (nItalic && !italic) + bold = !bold; + } + if (nItalic != italic) { ret.append((char) 0x1D); - if (nUnderline && !underline) + italic = !italic; + } + if (nUnderline != underline) { ret.append((char) 0x1F); - if (nFg != fg || bg != bg) { - ret.append((char) 0x03); - ret.append(nFg); - ret.append(','); - ret.append(nBg); + underline = !underline; + } + if (nStrikeout != strikeout) { + ret.append((char) 0x05); + strikeout = !strikeout; } - fg = nFg; - bg = nBg; - bold = nBold; - italic = nItalic; - underline = nUnderline; - ret.append(spannable, i, n); + ret.append(spannable, i, nextSpan); } return ret.toString(); }