diff --git a/build/package.json b/build/package.json index 45abeacb4..4cc5ac639 100644 --- a/build/package.json +++ b/build/package.json @@ -24,7 +24,7 @@ "grunt-peg": "~1.1.0", "grunt-shell": "~0.3.1", "harmony-collections": "~0.3.8", - "legal-eagle": "~0.6.0", + "legal-eagle": "~0.8.0", "minidump": "~0.8", "npm": "~1.4.5", "rcedit": "~0.3.0", diff --git a/package.json b/package.json index 990b1e626..10e69ff38 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "clear-cut": "0.4.0", "coffee-script": "1.8.0", "coffeestack": "0.8.0", + "color": "^0.7.3", "delegato": "^1", "emissary": "^1.3.1", "event-kit": "^1.0.1", diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 31e0756fc..c2a6f304f 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -1060,6 +1060,9 @@ describe "Config", -> type: 'integer' default: 12 + expect(atom.config.getSchema('foo.baz')).toBeUndefined() + expect(atom.config.getSchema('foo.bar.anInt.baz')).toBeUndefined() + it "respects the schema for scoped settings", -> schema = type: 'string' @@ -1287,6 +1290,79 @@ describe "Config", -> atom.config.set 'foo.bar', ['2', '3', '4'] expect(atom.config.get('foo.bar')).toEqual [2, 3, 4] + describe 'when the value has a "color" type', -> + beforeEach -> + schema = + type: 'color' + default: 'white' + atom.config.setSchema('foo.bar.aColor', schema) + + it 'returns a Color object', -> + color = atom.config.get('foo.bar.aColor') + expect(color.toHexString()).toBe '#ffffff' + expect(color.toRGBAString()).toBe 'rgba(255, 255, 255, 1)' + + color.red = 0 + color.green = 0 + color.blue = 0 + color.alpha = 0 + atom.config.set('foo.bar.aColor', color) + + color = atom.config.get('foo.bar.aColor') + expect(color.toHexString()).toBe '#000000' + expect(color.toRGBAString()).toBe 'rgba(0, 0, 0, 0)' + + color.red = 300 + color.green = -200 + color.blue = -1 + color.alpha = 'not see through' + atom.config.set('foo.bar.aColor', color) + + color = atom.config.get('foo.bar.aColor') + expect(color.toHexString()).toBe '#ff0000' + expect(color.toRGBAString()).toBe 'rgba(255, 0, 0, 1)' + + it 'coerces various types to a color object', -> + atom.config.set('foo.bar.aColor', 'red') + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 255, green: 0, blue: 0, alpha: 1} + atom.config.set('foo.bar.aColor', '#020') + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 0, green: 34, blue: 0, alpha: 1} + atom.config.set('foo.bar.aColor', '#abcdef') + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 171, green: 205, blue: 239, alpha: 1} + atom.config.set('foo.bar.aColor', 'rgb(1,2,3)') + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 1, green: 2, blue: 3, alpha: 1} + atom.config.set('foo.bar.aColor', 'rgba(4,5,6,.7)') + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 4, green: 5, blue: 6, alpha: .7} + atom.config.set('foo.bar.aColor', 'hsl(120,100%,50%)') + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 0, green: 255, blue: 0, alpha: 1} + atom.config.set('foo.bar.aColor', 'hsla(120,100%,50%,0.3)') + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 0, green: 255, blue: 0, alpha: .3} + atom.config.set('foo.bar.aColor', {red: 100, green: 255, blue: 2, alpha: .5}) + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 100, green: 255, blue: 2, alpha: .5} + atom.config.set('foo.bar.aColor', {red: 255}) + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 255, green: 0, blue: 0, alpha: 1} + atom.config.set('foo.bar.aColor', {red: 1000}) + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 255, green: 0, blue: 0, alpha: 1} + atom.config.set('foo.bar.aColor', {red: 'dark'}) + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 0, green: 0, blue: 0, alpha: 1} + + it 'reverts back to the default value when undefined is passed to set', -> + atom.config.set('foo.bar.aColor', undefined) + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 255, green: 255, blue: 255, alpha: 1} + + it 'will not set non-colors', -> + atom.config.set('foo.bar.aColor', null) + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 255, green: 255, blue: 255, alpha: 1} + + atom.config.set('foo.bar.aColor', 'nope') + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 255, green: 255, blue: 255, alpha: 1} + + atom.config.set('foo.bar.aColor', 30) + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 255, green: 255, blue: 255, alpha: 1} + + atom.config.set('foo.bar.aColor', false) + expect(atom.config.get('foo.bar.aColor')).toEqual {red: 255, green: 255, blue: 255, alpha: 1} + describe 'when the `enum` key is used', -> beforeEach -> schema = diff --git a/src/color.coffee b/src/color.coffee new file mode 100644 index 000000000..3dffabb48 --- /dev/null +++ b/src/color.coffee @@ -0,0 +1,87 @@ +_ = require 'underscore-plus' +ParsedColor = require 'color' + +# Essential: A simple color class returned from {Config::get} when the value +# at the key path is of type 'color'. +module.exports = +class Color + # Essential: Parse a {String} or {Object} into a {Color}. + # + # * `value` - A {String} such as `'white'`, `#ff00ff`, or + # `'rgba(255, 15, 60, .75)'` or an {Object} with `red`, `green`, + # `blue`, and `alpha` properties. + # + # Returns a {Color} or `null` if it cannot be parsed. + @parse: (value) -> + return null if _.isArray(value) or _.isFunction(value) + return null unless _.isObject(value) or _.isString(value) + + try + parsedColor = new ParsedColor(value) + catch error + return null + + new Color(parsedColor.red(), parsedColor.green(), parsedColor.blue(), parsedColor.alpha()) + + constructor: (red, green, blue, alpha) -> + Object.defineProperties this, + red: + set: (newRed) -> red = parseColor(newRed) + get: -> red + enumerable: true + configurable: false + green: + set: (newGreen) -> green = parseColor(newGreen) + get: -> green + enumerable: true + configurable: false + blue: + set: (newBlue) -> blue = parseColor(newBlue) + get: -> blue + enumerable: true + configurable: false + alpha: + set: (newAlpha) -> alpha = parseAlpha(newAlpha) + get: -> alpha + enumerable: true + configurable: false + + @red = red + @green = green + @blue = blue + @alpha = alpha + + # Esssential: Returns a {String} in the form `'#abcdef'`. + toHexString: -> + "##{numberToHexString(@red)}#{numberToHexString(@green)}#{numberToHexString(@blue)}" + + # Esssential: Returns a {String} in the form `'rgba(25, 50, 75, .9)'`. + toRGBAString: -> + "rgba(#{@red}, #{@green}, #{@blue}, #{@alpha})" + + isEqual: (color) -> + return true if this is color + color = Color.parse(color) unless color instanceof Color + return false unless color? + color.red is @red and color.blue is @blue and color.green is @green and color.alpha is @alpha + + clone: -> new Color(@red, @green, @blue, @alpha) + +parseColor = (color) -> + color = parseInt(color) + color = 0 if isNaN(color) + color = Math.max(color, 0) + color = Math.min(color, 255) + color + +parseAlpha = (alpha) -> + alpha = parseFloat(alpha) + alpha = 1 if isNaN(alpha) + alpha = Math.max(alpha, 0) + alpha = Math.min(alpha, 1) + alpha + +numberToHexString = (number) -> + hex = number.toString(16) + hex = "0#{hex}" if number < 10 + hex diff --git a/src/config.coffee b/src/config.coffee index 0f6498e14..721c522ad 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -8,6 +8,7 @@ async = require 'async' pathWatcher = require 'pathwatcher' Grim = require 'grim' +Color = require './color' ScopedPropertyStore = require 'scoped-property-store' ScopeDescriptor = require './scope-descriptor' @@ -216,6 +217,21 @@ ScopeDescriptor = require './scope-descriptor' # maximum: 11.5 # ``` # +# #### color +# +# Values will be coerced into a {Color} with `red`, `green`, `blue`, and `alpha` +# properties that all have numeric values. `red`, `green`, `blue` will be in +# the range 0 to 255 and `value` will be in the range 0 to 1. Values can be any +# valid CSS color format such as `#abc`, `#abcdef`, `white`, +# `rgb(50, 100, 150)`, and `rgba(25, 75, 125, .75)`. +# +# ```coffee +# config: +# someSetting: +# type: 'color' +# default: 'white' +# ``` +# # ### Other Supported Keys # # #### enum @@ -687,7 +703,7 @@ class Config schema = @schema for key in keys break unless schema? - schema = schema.properties[key] + schema = schema.properties?[key] schema # Deprecated: Returns a new {Object} containing all of the global settings and @@ -872,10 +888,16 @@ class Config defaultValue = _.valueForKeyPath(@defaultSettings, keyPath) if value? - value = _.deepClone(value) - _.defaults(value, defaultValue) if isPlainObject(value) and isPlainObject(defaultValue) + if value instanceof Color + value = value.clone() + else + value = _.deepClone(value) + _.defaults(value, defaultValue) if isPlainObject(value) and isPlainObject(defaultValue) else - value = _.deepClone(defaultValue) + if defaultValue instanceof Color + value = defaultValue.clone() + else + value = _.deepClone(defaultValue) value @@ -1103,6 +1125,13 @@ Config.addSchemaEnforcers else value + 'color': + coerce: (keyPath, value, schema) -> + color = Color.parse(value) + unless color? + throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} cannot be coerced into a color") + color + '*': coerceMinimumAndMaximum: (keyPath, value, schema) -> return value unless typeof value is 'number'