001 /*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2008-2017 the original author or authors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018 package griffon.javafx.formatters;
019
020 import griffon.core.formatters.AbstractFormatter;
021 import griffon.core.formatters.ParseException;
022 import javafx.scene.paint.Color;
023
024 import javax.annotation.Nonnull;
025 import javax.annotation.Nullable;
026 import java.lang.reflect.Field;
027 import java.util.Arrays;
028
029 import static griffon.util.GriffonNameUtils.isBlank;
030 import static java.lang.Integer.toHexString;
031 import static java.util.Objects.requireNonNull;
032
033 /**
034 * <p>A {@code Formatter} that can parse Strings into {@code javafx.scene.paint.Color} and back
035 * using several patterns</p>
036 * <p/>
037 * <p>
038 * Supported patterns are:
039 * <ul>
040 * <li>{@code #RGB}</li>
041 * <li>{@code #RGBA}</li>
042 * <li>{@code #RRGGBB}</li>
043 * <li>{@code #RRGGBBAA}</li>
044 * </ul>
045 * Where each letter stands for a particular color components in hexadecimal
046 * <ul>
047 * <li>{@code R} - red</li>
048 * <li>{@code G} - green</li>
049 * <li>{@code B} - blue</li>
050 * <li>{@code A} - alpha</li>
051 * </ul>
052 * </p>
053 *
054 * @author Andres Almiray
055 * @see griffon.core.formatters.Formatter
056 * @since 2.0.0
057 */
058 public class ColorFormatter extends AbstractFormatter<Color> {
059 /**
060 * "#RGB"
061 */
062 public static final String PATTERN_SHORT = "#RGB";
063
064 /**
065 * "#RGBA"
066 */
067 public static final String PATTERN_SHORT_WITH_ALPHA = "#RGBA";
068
069 /**
070 * "#RRGGBB"
071 */
072 public static final String PATTERN_LONG = "#RRGGBB";
073
074 /**
075 * "#RRGGBBAA"
076 */
077 public static final String PATTERN_LONG_WITH_ALPHA = "#RRGGBBAA";
078
079 /**
080 * "#RRGGBB"
081 */
082 public static final String DEFAULT_PATTERN = PATTERN_LONG;
083
084 private static final String[] PATTERNS = new String[]{
085 PATTERN_LONG,
086 PATTERN_LONG_WITH_ALPHA,
087 PATTERN_SHORT,
088 PATTERN_SHORT_WITH_ALPHA
089 };
090
091 /**
092 * {@code ColorFormatter} that uses the <b>{@code PATTERN_SHORT}</b> pattern
093 */
094 public static final ColorFormatter SHORT = new ColorFormatter(PATTERN_SHORT);
095
096 /**
097 * {@code ColorFormatter} that uses the <b>{@code PATTERN_SHORT_WITH_ALPHA}</b> pattern
098 */
099 public static final ColorFormatter SHORT_WITH_ALPHA = new ColorFormatter(PATTERN_SHORT_WITH_ALPHA);
100
101 /**
102 * {@code ColorFormatter} that uses the <b>{@code PATTERN_LONG}</b> pattern
103 */
104 public static final ColorFormatter LONG = new ColorFormatter(PATTERN_LONG);
105
106 /**
107 * {@code ColorFormatter} that uses the <b>{@code PATTERN_LONG_WITH_ALPHA}</b> pattern
108 */
109 public static final ColorFormatter LONG_WITH_ALPHA = new ColorFormatter(PATTERN_LONG_WITH_ALPHA);
110
111 private static final String ERROR_COLOR_NULL = "Cannot format given Color because it's null";
112 private static final String HASH = "#";
113 private static final String ZERO = "0";
114
115 /**
116 * <p>Returns a {@code ColorFormatter} given a color pattern.</p>
117 *
118 * @param pattern the input pattern. Must be one of the 4 supported color patterns.
119 *
120 * @return a {@code ColorPattern} instance
121 *
122 * @throws IllegalArgumentException if the supplied {@code pattern} is not supported
123 */
124 @Nonnull
125 public static ColorFormatter getInstance(@Nonnull String pattern) {
126 return new ColorFormatter(pattern);
127 }
128
129 private final ColorFormatterDelegate delegate;
130
131 protected ColorFormatter(@Nullable String pattern) {
132 if (PATTERN_SHORT.equals(pattern)) {
133 delegate = new ShortColorFormatterDelegate();
134 } else if (PATTERN_SHORT_WITH_ALPHA.equals(pattern)) {
135 delegate = new ShortWithAlphaColorFormatterDelegate();
136 } else if (PATTERN_LONG.equals(pattern)) {
137 delegate = new LongColorFormatterDelegate();
138 } else if (PATTERN_LONG_WITH_ALPHA.equals(pattern)) {
139 delegate = new LongWithAlphaColorFormatterDelegate();
140 } else if (isBlank(pattern)) {
141 delegate = new LongColorFormatterDelegate();
142 } else {
143 throw new IllegalArgumentException("Invalid pattern '" + pattern + "'. Valid patterns are " + Arrays.toString(PATTERNS));
144 }
145 }
146
147 @Override
148 @Nullable
149 public String format(@Nullable Color color) {
150 return color == null ? null : delegate.format(color);
151 }
152
153 @Override
154 @Nullable
155 public Color parse(@Nullable String str) throws ParseException {
156 return isBlank(str) ? null : delegate.parse(str);
157 }
158
159 /**
160 * Returns the pattern used by this {@code ColorFormatter}
161 *
162 * @return the pattern this {@code ColorFormatter} uses for parsing/formatting.
163 */
164 @Nonnull
165 public String getPattern() {
166 return delegate.getPattern();
167 }
168
169 /**
170 * <p>Parses a string into a {@code javafx.scene.paint.Color} instance.</p>
171 * <p>The parsing pattern is chosen given the length of the input string
172 * <ul>
173 * <li>4 - {@code #RGB}</li>
174 * <li>5 - {@code #RGBA}</li>
175 * <li>7 - {@code #RRGGBB}</li>
176 * <li>9 - {@code #RRGGBBAA}</li>
177 * </ul>
178 * </p>
179 * The input string may also be any of the Color constants identified by
180 * {@code javafx.scene.paint.Color}.
181 *
182 * @param str the string representation of a {@code javafx.scene.paint.Color}
183 *
184 * @return a {@code javafx.scene.paint.Color} instance matching the supplied RGBA color components
185 *
186 * @throws ParseException if the string cannot be parsed by the chosen pattern
187 * @see javafx.scene.paint.Color
188 */
189 @Nonnull
190 @SuppressWarnings("ConstantConditions")
191 public static Color parseColor(@Nonnull String str) throws ParseException {
192 if (str.startsWith(HASH)) {
193 switch (str.length()) {
194 case 4:
195 return SHORT.parse(str);
196 case 5:
197 return SHORT_WITH_ALPHA.parse(str);
198 case 7:
199 return LONG.parse(str);
200 case 9:
201 return LONG_WITH_ALPHA.parse(str);
202 default:
203 throw parseError(str, Color.class);
204 }
205 } else {
206 // assume it's a Color constant
207 try {
208 String colorFieldName = str.toUpperCase();
209 Field field = Color.class.getField(colorFieldName);
210 return (Color) field.get(null);
211 } catch (Exception e) {
212 throw parseError(str, Color.class, e);
213 }
214 }
215 }
216
217 private static interface ColorFormatterDelegate {
218 @Nonnull
219 String getPattern();
220
221 @Nonnull
222 String format(@Nonnull Color color);
223
224 @Nonnull
225 Color parse(@Nonnull String str) throws ParseException;
226 }
227
228 private abstract static class AbstractColorFormatterDelegate implements ColorFormatterDelegate {
229 private final String pattern;
230
231 private AbstractColorFormatterDelegate(@Nonnull String pattern) {
232 this.pattern = pattern;
233 }
234
235 @Override
236 @Nonnull
237 public String getPattern() {
238 return pattern;
239 }
240 }
241
242 private static int red(Color color) {
243 return toIntColor(color.getRed());
244 }
245
246 private static int green(Color color) {
247 return toIntColor(color.getGreen());
248 }
249
250 private static int blue(Color color) {
251 return toIntColor(color.getBlue());
252 }
253
254 private static int alpha(Color color) {
255 return toIntColor(color.getOpacity());
256 }
257
258 private static int toIntColor(double c) {
259 return (int) (c * 255);
260 }
261
262 private static class ShortColorFormatterDelegate extends AbstractColorFormatterDelegate {
263 private ShortColorFormatterDelegate() {
264 super(PATTERN_SHORT);
265 }
266
267 @Override
268 @Nonnull
269 public String format(@Nonnull Color color) {
270 requireNonNull(color, ERROR_COLOR_NULL);
271
272 return new StringBuilder(HASH)
273 .append(toHexString(red(color)).charAt(0))
274 .append(toHexString(green(color)).charAt(0))
275 .append(toHexString(blue(color)).charAt(0))
276 .toString();
277 }
278
279 @Override
280 @Nonnull
281 public Color parse(@Nonnull String str) throws ParseException {
282 if (!str.startsWith(HASH) || str.length() != 4) {
283 throw parseError(str, Color.class);
284 }
285
286 int r = parseHexInt(new StringBuilder()
287 .append(str.charAt(1))
288 .append(str.charAt(1))
289 .toString().toUpperCase(), Color.class);
290 int g = parseHexInt(new StringBuilder()
291 .append(str.charAt(2))
292 .append(str.charAt(2))
293 .toString().toUpperCase(), Color.class);
294 int b = parseHexInt(new StringBuilder()
295 .append(str.charAt(3))
296 .append(str.charAt(3))
297 .toString().toUpperCase(), Color.class);
298
299 return Color.rgb(r, g, b);
300 }
301 }
302
303 private static class ShortWithAlphaColorFormatterDelegate extends AbstractColorFormatterDelegate {
304 private ShortWithAlphaColorFormatterDelegate() {
305 super(PATTERN_SHORT_WITH_ALPHA);
306 }
307
308 @Override
309 @Nonnull
310 public String format(@Nonnull Color color) {
311 requireNonNull(color, ERROR_COLOR_NULL);
312
313 return new StringBuilder(HASH)
314 .append(toHexString(red(color)).charAt(0))
315 .append(toHexString(green(color)).charAt(0))
316 .append(toHexString(blue(color)).charAt(0))
317 .append(toHexString(alpha(color)).charAt(0))
318 .toString();
319 }
320
321 @Override
322 @Nonnull
323 public Color parse(@Nonnull String str) throws ParseException {
324 if (!str.startsWith(HASH) || str.length() != 5) {
325 throw parseError(str, Color.class);
326 }
327
328 int r = parseHexInt(new StringBuilder()
329 .append(str.charAt(1))
330 .append(str.charAt(1))
331 .toString().toUpperCase(), Color.class);
332 int g = parseHexInt(new StringBuilder()
333 .append(str.charAt(2))
334 .append(str.charAt(2))
335 .toString().toUpperCase(), Color.class);
336 int b = parseHexInt(new StringBuilder()
337 .append(str.charAt(3))
338 .append(str.charAt(3))
339 .toString().toUpperCase(), Color.class);
340 int a = parseHexInt(new StringBuilder()
341 .append(str.charAt(4))
342 .append(str.charAt(4))
343 .toString().toUpperCase(), Color.class);
344
345 return Color.rgb(r, g, b, a / 255d);
346 }
347 }
348
349 private static class LongColorFormatterDelegate extends AbstractColorFormatterDelegate {
350 private LongColorFormatterDelegate() {
351 super(PATTERN_LONG);
352 }
353
354 @Override
355 @Nonnull
356 public String format(@Nonnull Color color) {
357 requireNonNull(color, ERROR_COLOR_NULL);
358
359 return new StringBuilder(HASH)
360 .append(padLeft(toHexString(red(color)), ZERO))
361 .append(padLeft(toHexString(green(color)), ZERO))
362 .append(padLeft(toHexString(blue(color)), ZERO))
363 .toString();
364 }
365
366 @Override
367 @Nonnull
368 public Color parse(@Nonnull String str) throws ParseException {
369 if (!str.startsWith(HASH) || str.length() != 7) {
370 throw parseError(str, Color.class);
371 }
372
373 int r = parseHexInt(new StringBuilder()
374 .append(str.charAt(1))
375 .append(str.charAt(2))
376 .toString().toUpperCase(), Color.class);
377 int g = parseHexInt(new StringBuilder()
378 .append(str.charAt(3))
379 .append(str.charAt(4))
380 .toString().toUpperCase(), Color.class);
381 int b = parseHexInt(new StringBuilder()
382 .append(str.charAt(5))
383 .append(str.charAt(6))
384 .toString().toUpperCase(), Color.class);
385
386 return Color.rgb(r, g, b);
387 }
388 }
389
390 private static class LongWithAlphaColorFormatterDelegate extends AbstractColorFormatterDelegate {
391 private LongWithAlphaColorFormatterDelegate() {
392 super(PATTERN_LONG_WITH_ALPHA);
393 }
394
395 @Override
396 @Nonnull
397 public String format(@Nonnull Color color) {
398 requireNonNull(color, ERROR_COLOR_NULL);
399
400 return new StringBuilder(HASH)
401 .append(padLeft(toHexString(red(color)), ZERO))
402 .append(padLeft(toHexString(green(color)), ZERO))
403 .append(padLeft(toHexString(blue(color)), ZERO))
404 .append(padLeft(toHexString(alpha(color)), ZERO))
405 .toString();
406 }
407
408 @Override
409 @Nonnull
410 public Color parse(@Nonnull String str) throws ParseException {
411 if (!str.startsWith(HASH) || str.length() != 9) {
412 throw parseError(str, Color.class);
413 }
414
415 int r = parseHexInt(new StringBuilder()
416 .append(str.charAt(1))
417 .append(str.charAt(2))
418 .toString().toUpperCase(), Color.class);
419 int g = parseHexInt(new StringBuilder()
420 .append(str.charAt(3))
421 .append(str.charAt(4))
422 .toString().toUpperCase(), Color.class);
423 int b = parseHexInt(new StringBuilder()
424 .append(str.charAt(5))
425 .append(str.charAt(6))
426 .toString().toUpperCase(), Color.class);
427 int a = parseHexInt(new StringBuilder()
428 .append(str.charAt(7))
429 .append(str.charAt(8))
430 .toString().toUpperCase(), Color.class);
431
432 return Color.rgb(r, g, b, a / 255d);
433 }
434 }
435
436 private static String padLeft(String self, String padding) {
437 return 2 <= self.length() ? self : padding + self;
438 }
439 }
|