From 5dcfecf9f848955a84b4f782f64c696ac48173b2 Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Sat, 13 Sep 2025 00:17:24 -0400 Subject: [PATCH] Icon Color Select Dropdown --- vite/src/pipedal/App.tsx | 7 + vite/src/pipedal/IconColorDropdownButton.tsx | 142 +++++++++++++++++++ vite/src/pipedal/IconTest.tsx | 73 ++++++++++ vite/src/pipedal/PluginIcon.tsx | 61 +++++++- vite/src/pipedal/svg/fx_spectral.svg | 53 +++---- 5 files changed, 307 insertions(+), 29 deletions(-) create mode 100644 vite/src/pipedal/IconColorDropdownButton.tsx create mode 100644 vite/src/pipedal/IconTest.tsx diff --git a/vite/src/pipedal/App.tsx b/vite/src/pipedal/App.tsx index 396d281..b8a60f5 100644 --- a/vite/src/pipedal/App.tsx +++ b/vite/src/pipedal/App.tsx @@ -28,6 +28,7 @@ import { isDarkMode } from './DarkMode'; import Tone3000AuthComplete from './Tone3000AuthComplete'; import FontTest from './FontTest'; +import IconTest from './IconTest'; declare module '@mui/material/styles' { interface Theme { @@ -235,6 +236,11 @@ function isFontTest() { let param = url.searchParams.get("fontTest"); return (param !== null) } +function isIconTest() { + let url = new URL(window.location.href); + let param = url.searchParams.get("iconTest"); + return (param !== null) +} const App = (class extends React.Component { // Before the component mounts, we initialise our state @@ -258,6 +264,7 @@ const App = (class extends React.Component { { isTone3000Auth() && () || isFontTest() && () + || isIconTest() && () || () } diff --git a/vite/src/pipedal/IconColorDropdownButton.tsx b/vite/src/pipedal/IconColorDropdownButton.tsx new file mode 100644 index 0000000..eb1c09f --- /dev/null +++ b/vite/src/pipedal/IconColorDropdownButton.tsx @@ -0,0 +1,142 @@ +/* + * MIT License + * + * Copyright (c) Robin E. R. Davies + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React, { useState, useEffect } from 'react'; +import ButtonBase from '@mui/material/ButtonBase'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import { useTheme } from '@mui/material/styles'; +import { getIconColorSelections,getIconColor,IconColorSelect } from './PluginIcon'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; + + +export enum DropdownAlignment { + SE, SW +} + +interface ColorListDropdownButtonProps { + colorKey: string; + colorList?: IconColorSelect[]; + + onColorChange: (selection: IconColorSelect) => void; + dropdownAlignment: DropdownAlignment; +} + +const IconColorDropdownButton: React.FC = (props: ColorListDropdownButtonProps) => { + let { colorList, colorKey, onColorChange, dropdownAlignment } = props; + if (!colorList) colorList = getIconColorSelections(); + const theme = useTheme(); + + const [anchorEl, setAnchorEl] = useState(null); + + let selectedColor = getIconColor(colorKey); + if (selectedColor === undefined) { + selectedColor = theme.palette.text.secondary; + } + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleColorSelect = (selection: IconColorSelect) => { + handleClose(); + onColorChange(selection); + }; + return ( +
+ +
+
+ +
+ + + +
+ + handleColorSelect({key: "default", value: undefined})} + style={{ + width: 48, height: 48, borderRadius: 6, margin: 4, + borderStyle: "solid", + borderWidth: colorKey ==="default" ? 2 : 0.25, + borderColor: colorKey == "default" ? + theme.palette.text.primary + : theme.palette.text.secondary, + backgroundColor: theme.palette.text.secondary + }} + > + +
+ {colorList.map((color) => { + let selected = color.key === selectedColor; + return ( + handleColorSelect(color)} + style={{ + width: 48, height: 48, borderRadius: 6, margin: 4, + borderStyle: "solid", + borderWidth: selected ? 2 : 0.25, + borderColor: color.key === selectedColor ? + theme.palette.text.primary + : theme.palette.text.secondary, + backgroundColor: color.value + }} + > + ) + } + )} +
+
+
+
+ ); +}; + +export default IconColorDropdownButton; \ No newline at end of file diff --git a/vite/src/pipedal/IconTest.tsx b/vite/src/pipedal/IconTest.tsx new file mode 100644 index 0000000..0a40d24 --- /dev/null +++ b/vite/src/pipedal/IconTest.tsx @@ -0,0 +1,73 @@ + +import { useState } from "react"; +import { PluginType } from "./Lv2Plugin"; +import PluginIcon, { getIconColorSelections } from './PluginIcon'; +import IconColorDropdownButton, {DropdownAlignment} from "./IconColorDropdownButton"; + +function IconTest() { + const [selectedColor, setSelectedColor] = useState(undefined); + const [selectedKey, setSelectedKey] = useState(""); + return ( + <> +
+ { + getIconColorSelections().map((color) => { + return ( +
{ + setSelectedColor(color.value ); + }} + /> + ); + + }) + } + +
+
+ { + getIconColorSelections(false).map((color) => { + return ( +
{ setSelectedColor(color.value ); }} + /> + ); + + }) + } + +
+
+ + { + setSelectedColor(selection.value); + setSelectedKey(selection.key); + } + } + dropdownAlignment={DropdownAlignment.SE} /> +
+
+ { + Object.values(PluginType).map((pluginType) => ( +
+ +
+ )) + } +
+
+ { + Object.values(PluginType).map((pluginType) => ( +
+ +
+ )) + } +
+ + ); +} + +export default IconTest; \ No newline at end of file diff --git a/vite/src/pipedal/PluginIcon.tsx b/vite/src/pipedal/PluginIcon.tsx index e034eb5..82b8925 100644 --- a/vite/src/pipedal/PluginIcon.tsx +++ b/vite/src/pipedal/PluginIcon.tsx @@ -68,6 +68,59 @@ import FxTerminalIcon from './svg/fx_terminal.svg?react'; import { isDarkMode } from './DarkMode'; +export interface IconColorSelect { + key: string, + value: string | undefined, +}; + +export const getIconColorSelections = (darkMode?: boolean): IconColorSelect[] => { + if (darkMode === undefined) darkMode = isDarkMode(); + let result: IconColorSelect[] = []; + + let l: number; + let c: number; + if (darkMode) { + l = 0.8; + c = 0.25; + } else { + l = 0.5; + c = 0.25; + } + + for (let angle = 0; angle < 360; angle += 22.5) { + result.push( + { + key: "oklch" + Math.round(angle*10).toString(), + value: ("oklch(" + l.toString() + " " + c.toString() + " " + (angle).toString() + ")") + + } + ); + } + return result; + +}; + + +function makeDefaultColorMap(): {[key:string]: string} { + let result: {[key:string]: string} = {}; + for (let selection of getIconColorSelections()) + { + result[selection.key] = selection.value??""; + } + return result; +} + +const iconColors: {[key:string]: string} = makeDefaultColorMap(); + +export const getIconColor = (key?: string): string| undefined => { + if (iconColors.hasOwnProperty(key!)) + { + return iconColors[key!]; + } + return undefined; +} + + const styles = (theme: Theme) => @@ -86,13 +139,13 @@ const styles = (theme: Theme) => export interface PluginIconProps extends WithStyles { size?: number; opacity?: number; + color?: string; offsetY?: number; pluginType: PluginType; pluginMissing?: boolean; } -export function SelectSvgIcon(plugin_type: PluginType, className: string, size: number = 24, opacity: number = 1.0, missingPlugin: boolean = false) { - let color = ""; +export function SelectSvgIcon(plugin_type: PluginType, className: string, size: number = 24, opacity: number = 1.0, missingPlugin: boolean = false, color = "") { if (missingPlugin) { color = isDarkMode() ? "#C00000" : "#B00000"; } @@ -268,7 +321,7 @@ export function SelectIconUri(plugin_type: PluginType) { } const PluginIcon = withStyles((props: PluginIconProps) => { - const { pluginType, opacity, pluginMissing } = props; + const { pluginType, opacity, pluginMissing,color } = props; const classes = withStyles.getClasses(props); let pluginMissing_: boolean = pluginMissing ?? false; @@ -277,7 +330,7 @@ const PluginIcon = withStyles((props: PluginIconProps) => { if (props.size) size = props.size; let topVal: number = (props.offsetY ?? 0); - let svgIcon = SelectSvgIcon(pluginType, classes.icon, size, opacity, pluginMissing_); + let svgIcon = SelectSvgIcon(pluginType, classes.icon, size, opacity, pluginMissing_,color? color: "" ); if (svgIcon) { return svgIcon; } diff --git a/vite/src/pipedal/svg/fx_spectral.svg b/vite/src/pipedal/svg/fx_spectral.svg index dadd1a1..5f08cc2 100644 --- a/vite/src/pipedal/svg/fx_spectral.svg +++ b/vite/src/pipedal/svg/fx_spectral.svg @@ -1,25 +1,28 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + +