diff --git a/README.md b/README.md index 51e10f19c7..c614def1f9 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,14 @@ After installation or update you can click on the 'Activate'-button to activate activation +## Custom icon saturation + +If colors do not make you happy you can change icons to have less saturation making them look grayish or completely grayscale by setting saturation to 0: + +```json +"material-icon-theme.saturation": 0.5 +``` + ## Commands Press `Ctrl-Shift-P` to open the command palette and type `Material Icons`. @@ -119,6 +127,10 @@ Press `Ctrl-Shift-P` to open the command palette and type `Material Icons`. - **Restore Default Configuration**: Reset the default configurations of the icon theme. +- **Toggle Grayscale**: Change icons to saturation to 0 making them look grayscale. + +- **Change Saturation**: Change the saturation value of the icons. + ## Icon sources * [Material Design Icons](https://materialdesignicons.com/) diff --git a/package.json b/package.json index 91dfb645e3..648237325d 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,14 @@ { "command": "material-icon-theme.opacity", "title": "%command.opacity%" + }, + { + "command": "material-icon-theme.grayscale", + "title": "%command.grayscale%" + }, + { + "command": "material-icon-theme.saturation", + "title": "%command.saturation%" } ], "configuration": { @@ -157,6 +165,13 @@ "type": "boolean", "default": false, "description": "%configuration.hidesExplorerArrows%" + }, + "material-icon-theme.saturation": { + "type": "number", + "default": 1, + "minimum": 0, + "maximum": 1, + "description": "%configuration.saturation%" } } } @@ -195,3 +210,4 @@ "vscode": "^1.1.29" } } + diff --git a/package.nls.json b/package.nls.json index 59f8abd611..2444fe4c4b 100644 --- a/package.nls.json +++ b/package.nls.json @@ -6,6 +6,8 @@ "command.restoreDefaultConfig": "Material Icons: Restore Default Configuration", "command.hidesExplorerArrows": "Material Icons: Hide Folder Arrows", "command.opacity": "Material Icons: Change Opacity", + "command.grayscale": "Material Icons: Toggle Grayscale", + "command.saturation": "Material Icons: Change Saturation", "configuration.title": "Material Icons", "configuration.files.associations": "Set custom file icon associations.", "configuration.folders.associations": "Set custom folder icon associations.", @@ -25,5 +27,6 @@ "configuration.folders.theme.none": "No folder icons.", "configuration.folders.color": "Change the color of the folder icons.", "configuration.hidesExplorerArrows": "Hide explorer arrows before folder.", - "configuration.opacity": "Change the opacity of the icons." + "configuration.opacity": "Change the opacity of the icons.", + "configuration.saturation": "Change the saturation of the icons." } \ No newline at end of file diff --git a/src/commands/grayscale.ts b/src/commands/grayscale.ts new file mode 100644 index 0000000000..dfb4a4a3e8 --- /dev/null +++ b/src/commands/grayscale.ts @@ -0,0 +1,53 @@ +import * as vscode from 'vscode'; +import * as helpers from './../helpers'; +import * as i18n from './../i18n'; + +/** Command to toggle grayscale. */ +export const toggleGrayscale = () => { + return checkGrayscaleStatus() + .then(showQuickPickItems) + .then(handleQuickPickActions) + .catch(err => console.log(err)); +}; + +/** Show QuickPick items to select preferred configuration for grayscale icons. */ +const showQuickPickItems = (status: boolean) => { + const on: vscode.QuickPickItem = { + description: i18n.translate('toggleSwitch.on'), + detail: i18n.translate(`grayscale.enableGrayscale`), + label: status ? '\u2714' : '\u25FB' + }; + const off: vscode.QuickPickItem = { + description: i18n.translate('toggleSwitch.off'), + detail: i18n.translate(`grayscale.disableGrayscale`), + label: !status ? '\u2714' : '\u25FB' + }; + return vscode.window.showQuickPick( + [on, off], { + placeHolder: i18n.translate('grayscale.toggleGrayscale'), + ignoreFocusOut: false, + matchOnDescription: true + }); +}; + +/** Handle the actions from the QuickPick. */ +const handleQuickPickActions = (value: vscode.QuickPickItem) => { + if (!value || !value.description) return; + switch (value.description) { + case i18n.translate('toggleSwitch.on'): { + helpers.setThemeConfig('saturation', 0, true); + break; + } + case i18n.translate('toggleSwitch.off'): { + helpers.setThemeConfig('saturation', 1, true); + break; + } + default: + break; + } +}; + +/** Is grayscale icons enabled? */ +export const checkGrayscaleStatus = (): Promise => { + return helpers.getMaterialIconsJSON().then((config) => config.options.saturation === 0); +}; diff --git a/src/commands/index.ts b/src/commands/index.ts index aa255012f1..77ec0b3386 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -6,6 +6,8 @@ import { changeFolderTheme } from './folders'; import { toggleIconPacks } from './iconPacks'; import { changeOpacity } from './opacity'; import { restoreDefaultConfig } from './restoreConfig'; +import { toggleGrayscale } from './grayscale'; +import { changeSaturation } from './saturation'; // Activate theme const activateThemeCommand = vscode.commands.registerCommand('material-icon-theme.activateIcons', () => { @@ -42,6 +44,16 @@ const changeOpacityCommand = vscode.commands.registerCommand('material-icon-them changeOpacity(); }); +// Toggle grayscale icons +const grayscaleCommand = vscode.commands.registerCommand('material-icon-theme.grayscale', () => { + toggleGrayscale(); +}); + +// Change the saturation of the icons +const changeSaturationCommand = vscode.commands.registerCommand('material-icon-theme.saturation', () => { + changeSaturation(); +}); + export const commands = [ activateThemeCommand, toggleIconPacksCommand, @@ -49,5 +61,7 @@ export const commands = [ toggleFolderColorCommand, restoreDefaultConfigCommand, hidesExplorerArrowsCommand, - changeOpacityCommand + changeOpacityCommand, + grayscaleCommand, + changeSaturationCommand ]; diff --git a/src/commands/restoreConfig.ts b/src/commands/restoreConfig.ts index 1f07005700..4f1652388c 100644 --- a/src/commands/restoreConfig.ts +++ b/src/commands/restoreConfig.ts @@ -7,6 +7,7 @@ export const restoreDefaultConfig = () => { helpers.setThemeConfig('folders.color', undefined, true); helpers.setThemeConfig('hidesExplorerArrows', undefined, true); helpers.setThemeConfig('opacity', undefined, true); + helpers.setThemeConfig('saturation', undefined, true); helpers.setThemeConfig('files.associations', undefined, true); helpers.setThemeConfig('folders.associations', undefined, true); helpers.setThemeConfig('languages.associations', undefined, true); diff --git a/src/commands/saturation.ts b/src/commands/saturation.ts new file mode 100644 index 0000000000..19016e1dd5 --- /dev/null +++ b/src/commands/saturation.ts @@ -0,0 +1,43 @@ +import * as vscode from 'vscode'; +import { getDefaultIconOptions, validateSaturationValue } from '../icons'; +import * as helpers from './../helpers'; +import * as i18n from './../i18n'; + +/** Command to toggle the folder icons. */ +export const changeSaturation = () => { + return getCurrentSaturationValue() + .then(showInput) + .catch(err => console.log(err)); +}; + +/** Show input to enter the saturation value. */ +const showInput = (saturation: number) => { + vscode.window.showInputBox({ + placeHolder: i18n.translate('saturation.inputPlaceholder'), + ignoreFocusOut: true, + value: String(saturation), + validateInput: validateSaturationInput + }).then(value => setSaturationConfig(+value)); +}; + +/** Validate the saturation value which was inserted by the user. */ +const validateSaturationInput = (saturationInput: string) => { + if (!validateSaturationValue(+saturationInput)) { + return i18n.translate('saturation.wrongValue'); + } + return undefined; +}; + +/** Get the current value of the saturation of the icons. */ +export const getCurrentSaturationValue = (): Promise => { + const defaultOptions = getDefaultIconOptions(); + return helpers.getMaterialIconsJSON().then((config) => + config.options.saturation === undefined ? + defaultOptions.saturation : config.options.saturation); +}; + +const setSaturationConfig = (saturation: number) => { + if (saturation !== undefined) { + helpers.setThemeConfig('saturation', saturation, true); + } +}; diff --git a/src/i18n/lang-en.ts b/src/i18n/lang-en.ts index 000f176f4a..9b72db4848 100644 --- a/src/i18n/lang-en.ts +++ b/src/i18n/lang-en.ts @@ -39,5 +39,14 @@ export const translation: Translation = { 'confirmReload': 'You have to restart VS Code to activate the changes to the icons.', 'reload': 'Restart', 'outdatedVersion': 'You have to update VS Code to use this command.', - 'updateVSCode': 'Update VS Code' + 'updateVSCode': 'Update VS Code', + 'grayscale': { + 'toggleGrayscale': 'Toggle grayscale icons', + 'enableGrayscale': 'Enable grayscale icons', + 'disableGrayscale': 'Disable grayscale icons' + }, + 'saturation': { + 'inputPlaceholder': 'Saturation value (between 0 and 1)', + 'wrongValue': 'The value must be between 0 and 1!', + } }; diff --git a/src/icons/generator/iconSaturation.ts b/src/icons/generator/iconSaturation.ts new file mode 100644 index 0000000000..33accb6146 --- /dev/null +++ b/src/icons/generator/iconSaturation.ts @@ -0,0 +1,132 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Changes saturation of all icons in the set. + * @param saturation Saturation value. + * @param fileNames Only change the saturation of certain file names. + */ +export const setIconSaturation = (saturation: number, fileNames?: string[]) => { + if (!validateSaturationValue(saturation)) { + return console.error('Invalid saturation value! Saturation must be a decimal number between 0 and 1!'); + } + + return new Promise((resolve, reject) => { + let iconsPath = path.join(__dirname, '..', '..', '..'); + const parentFolder = iconsPath.split(path.sep).pop(); + if (parentFolder === 'out') { + iconsPath = path.join(iconsPath, '..'); + } + iconsPath = path.join(iconsPath, 'icons'); + + // read all icon files from the icons folder + try { + (fileNames || fs.readdirSync(iconsPath)).forEach(iconFileName => { + const svgFilePath = path.join(iconsPath, iconFileName); + + // Read SVG file + const svg = fs.readFileSync(svgFilePath, 'utf-8'); + + // Get the root element of the SVG file + const svgRootElement = getSVGRootElement(svg); + if (!svgRootElement) return; + + let updatedRootElement: string; + if (saturation < 1) { + updatedRootElement = addFilterAttribute(svgRootElement); + } else { + updatedRootElement = removeFilterAttribute(svgRootElement); + } + let updatedSVG = svg.replace(/]*>/, updatedRootElement); + if (saturation < 1) { + updatedSVG = addFilterElement(updatedSVG, saturation); + } else { + updatedSVG = removeFilterElement(updatedSVG); + } + + fs.writeFileSync(svgFilePath, updatedSVG); + resolve(); + }); + } + catch (e) { + console.log(e); + reject(e); + } + resolve(); + }); +}; + +/** + * Get the SVG root element. + * @param svg SVG file as string. + */ +const getSVGRootElement = (svg: string) => { + const result = new RegExp(/]*>/).exec(svg); + if (result.length > 0) { + return result[0]; + } else { + return undefined; + } +}; + +/** + * Add an filter attribute to the SVG icon. + * @param svgRoot Root element of the SVG icon. + */ +const addFilterAttribute = (svgRoot: string) => { + const pattern = new RegExp(/\sfilter="[^"]+?"/); + // if the filter attribute already exists + if (pattern.test(svgRoot)) { + return svgRoot.replace(pattern, ` filter="url(#saturation)"`); + } else { + return svgRoot.replace(/^ { + const pattern = new RegExp(/\sfilter="[^"]+?"/); + // check if the filter attribute exists + if (pattern.test(svgRoot)) { + return svgRoot.replace(pattern, ''); + } + return svgRoot; +}; + +/** + * Add filter element to the SVG icon. + * @param svg SVG file as string. + */ +const addFilterElement = (svg: string, value: number) => { + const pattern = new RegExp(/(.*<\/svg>)/); + const filterElement = ``; + if (pattern.test(svg)) { + return svg.replace(pattern, `${filterElement}$1`); + } else { + return svg.replace(/<\/svg>/, `${filterElement}`); + } + return svg; +}; + +/** + * Remove filter element from the SVG icon. + * @param svg SVG file as string. + */ +const removeFilterElement = (svg: string) => { + const pattern = new RegExp(/(.*<\/svg>)/); + if (pattern.test(svg)) { + return svg.replace(pattern, `$1`); + } + return svg; +}; + +/** + * Validate the saturation value. + * @param saturation Saturation value + */ +export const validateSaturationValue = (saturation: number) => { + return saturation !== null && saturation <= 1 && saturation >= 0; +}; diff --git a/src/icons/generator/index.ts b/src/icons/generator/index.ts index 0bda060bbd..005e431f84 100644 --- a/src/icons/generator/index.ts +++ b/src/icons/generator/index.ts @@ -4,3 +4,4 @@ export * from './languageGenerator'; export * from './constants'; export * from './jsonGenerator'; export * from './iconOpacity'; +export * from './iconSaturation'; diff --git a/src/icons/generator/jsonGenerator.ts b/src/icons/generator/jsonGenerator.ts index be56f74416..4805e9ad1f 100644 --- a/src/icons/generator/jsonGenerator.ts +++ b/src/icons/generator/jsonGenerator.ts @@ -6,7 +6,7 @@ import { fileIcons } from '../fileIcons'; import { folderIcons } from '../folderIcons'; import { languageIcons } from '../languageIcons'; import { iconJsonName } from './constants'; -import { generateFolderIcons, getFileIconDefinitions, getFolderIconDefinitions, getLanguageIconDefinitions, setIconOpacity, validateHEXColorCode, validateOpacityValue } from './index'; +import { generateFolderIcons, getFileIconDefinitions, getFolderIconDefinitions, getLanguageIconDefinitions, setIconOpacity, setIconSaturation, validateHEXColorCode, validateOpacityValue, validateSaturationValue } from './index'; /** * Generate the complete icon configuration object that can be written as JSON file. @@ -32,9 +32,14 @@ export const createIconFile = async (updatedConfigs?: IconJsonOptions, updatedJS const iconJSONPath = path.join(__dirname, '../../../', 'src', iconJsonName); const json = generateIconConfigurationObject(options); - // make sure that the opacity value must be entered correctly to trigger a reload. - if (updatedConfigs && updatedConfigs.opacity !== undefined && !validateOpacityValue(updatedConfigs.opacity)) { - return Promise.reject('Material Icons: Invalid opacity value!'); + // make sure that the opacity and saturation values must be entered correctly to trigger a reload. + if (updatedConfigs) { + if (updatedConfigs.opacity !== undefined && !validateOpacityValue(updatedConfigs.opacity)) { + return Promise.reject('Material Icons: Invalid opacity value!'); + } + if (updatedConfigs.saturation !== undefined && !validateSaturationValue(updatedConfigs.saturation)) { + return Promise.reject('Material Icons: Invalid saturation value!'); + } } // make sure that the value for the folder color is entered correctly to trigger a reload. @@ -61,6 +66,9 @@ export const createIconFile = async (updatedConfigs?: IconJsonOptions, updatedJS if (!updatedConfigs || updatedConfigs.opacity !== undefined) { await setIconOpacity(options.opacity); } + if (!updatedConfigs || updatedConfigs.saturation !== undefined) { + await setIconSaturation(options.saturation); + } }); } catch (error) { throw Error(error); @@ -80,6 +88,7 @@ export const getDefaultIconOptions = (): IconJsonOptions => ({ activeIconPack: 'angular', hidesExplorerArrows: false, opacity: 1, + saturation: 1, files: { associations: {} }, languages: { associations: {} }, }); diff --git a/src/models/i18n/translation.ts b/src/models/i18n/translation.ts index 79d68d06b3..827bc32d86 100644 --- a/src/models/i18n/translation.ts +++ b/src/models/i18n/translation.ts @@ -38,4 +38,13 @@ export interface Translation { reload?: string; outdatedVersion?: string; updateVSCode?: string; + grayscale?: { + toggleGrayscale?: string; + enableGrayscale?: string; + disableGrayscale?: string; + }; + saturation?: { + inputPlaceholder?: string; + wrongValue?: string; + }; } diff --git a/src/models/icons/iconJsonOptions.ts b/src/models/icons/iconJsonOptions.ts index 18f645fcf7..dd757bfd43 100644 --- a/src/models/icons/iconJsonOptions.ts +++ b/src/models/icons/iconJsonOptions.ts @@ -2,6 +2,7 @@ export interface IconJsonOptions { activeIconPack?: string; hidesExplorerArrows?: boolean; opacity?: number; + saturation?: number; folders?: { theme?: string; color?: string;