File: System\Drawing\Design\ColorEditor.ColorPalette.cs
Web Access
Project: src\src\System.Windows.Forms.Design\src\System.Windows.Forms.Design.csproj (System.Windows.Forms.Design)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Windows.Forms;
 
namespace System.Drawing.Design;
 
public partial class ColorEditor
{
    /// <summary>
    ///  A control to display the color palette.
    /// </summary>
    private partial class ColorPalette : Control
    {
        public const int CellsAcross = 8;
        public const int CellsDown = 8;
        public const int CellsCustom = 16; // last cells
        public const int TotalCells = CellsAcross * CellsDown;
        public const int CellSize = 16;
        public const int MarginWidth = 8;
 
        private static bool s_isScalingInitialized;
        private static int s_cellSizeX = CellSize;
        private static int s_cellSizeY = CellSize;
        private static int s_marginX = MarginWidth;
        private static int s_marginY = MarginWidth;
 
        private static readonly int[] s_staticCells =
        [
            0x00ffffff, 0x00c0c0ff, 0x00c0e0ff, 0x00c0ffff,
            0x00c0ffc0, 0x00ffffc0, 0x00ffc0c0, 0x00ffc0ff,
 
            0x00e0e0e0, 0x008080ff, 0x0080c0ff, 0x0080ffff,
            0x0080ff80, 0x00ffff80, 0x00ff8080, 0x00ff80ff,
 
            0x00c0c0c0, 0x000000ff, 0x000080ff, 0x0000ffff,
            0x0000ff00, 0x00ffff00, 0x00ff0000, 0x00ff00ff,
 
            0x00808080, 0x000000c0, 0x000040c0, 0x0000c0c0,
            0x0000c000, 0x00c0c000, 0x00c00000, 0x00c000c0,
 
            0x00404040, 0x00000080, 0x00004080, 0x00008080,
            0x00008000, 0x00808000, 0x00800000, 0x00800080,
 
            0x00000000, 0x00000040, 0x00404080, 0x00004040,
            0x00004000, 0x00404000, 0x00400000, 0x00400040
        ];
 
        private readonly Color[] _staticColors;
        private Color _selectedColor;
        private Point _focus;
        protected EventHandler? _onPicked;
        private readonly ColorUI _colorUI;
 
        public ColorPalette(ColorUI colorUI, Color[] customColors)
        {
            if (!s_isScalingInitialized)
            {
                s_cellSizeX = ScaleHelper.ScaleToInitialSystemDpi(CellSize);
                s_cellSizeY = ScaleHelper.ScaleToInitialSystemDpi(CellSize);
                s_marginX = ScaleHelper.ScaleToInitialSystemDpi(MarginWidth);
                s_marginY = ScaleHelper.ScaleToInitialSystemDpi(MarginWidth);
 
                s_isScalingInitialized = true;
            }
 
            _colorUI = colorUI;
            SetStyle(ControlStyles.Opaque, true);
 
            BackColor = SystemColors.Control;
 
            Size = new Size(
                CellsAcross * (s_cellSizeX + s_marginX) + s_marginX + 2,
                CellsDown * (s_cellSizeY + s_marginY) + s_marginY + 2);
 
            _staticColors = new Color[TotalCells - CellsCustom];
 
            for (int i = 0; i < s_staticCells.Length; i++)
            {
                _staticColors[i] = ColorTranslator.FromOle(s_staticCells[i]);
            }
 
            CustomColors = customColors;
        }
 
        public Color[] CustomColors { get; }
 
        internal int FocusedCell => Get1DFrom2D(_focus);
 
        public Color SelectedColor
        {
            get => _selectedColor;
            set
            {
                if (!value.Equals(_selectedColor))
                {
                    InvalidateSelection();
                    _selectedColor = value;
 
                    SetFocus(GetCellFromColor(value));
                    InvalidateSelection();
                }
            }
        }
 
        public event EventHandler Picked
        {
            add => _onPicked += value;
            remove => _onPicked -= value;
        }
 
        protected override AccessibleObject CreateAccessibilityInstance() => new ColorPaletteAccessibleObject(this);
 
        protected EventHandler? Get_onPicked()
        {
            return _onPicked;
        }
 
        protected void OnPicked(EventArgs e, EventHandler? onPicked) => onPicked?.Invoke(this, e);
 
        private static void FillRectWithCellBounds(int across, int down, ref Rectangle rect)
        {
            rect.X = s_marginX + across * (s_cellSizeX + s_marginX);
            rect.Y = s_marginY + down * (s_cellSizeY + s_marginY);
            rect.Width = s_cellSizeX;
            rect.Height = s_cellSizeY;
        }
 
        private Point GetCellFromColor(Color c)
        {
            for (int y = 0; y < CellsDown; y++)
            {
                for (int x = 0; x < CellsAcross; x++)
                {
                    if (GetColorFromCell(x, y).Equals(c))
                    {
                        return new Point(x, y);
                    }
                }
            }
 
            return Point.Empty;
        }
 
        private Color GetColorFromCell(int across, int down) => GetColorFromCell(Get1DFrom2D(across, down));
 
        private Color GetColorFromCell(int index)
        {
            if (index < TotalCells - CellsCustom)
            {
                return _staticColors[index];
            }
 
            return CustomColors[index - TotalCells + CellsCustom];
        }
 
        private static Point GetCell2DFromLocationMouse(int x, int y)
        {
            int across = x / (s_cellSizeX + s_marginX);
            int down = y / (s_cellSizeY + s_marginY);
 
            // Check if we're outside the cells
            if (across < 0 || down < 0 || across >= CellsAcross || down >= CellsDown)
            {
                return new Point(-1, -1);
            }
 
            // Check if we're in the margin
            if ((x - (s_cellSizeX + s_marginX) * across) < s_marginX
                || (y - (s_cellSizeY + s_marginY) * down) < s_marginY)
            {
                return new Point(-1, -1);
            }
 
            return new Point(across, down);
        }
 
        private static int GetCellFromLocationMouse(int x, int y) => Get1DFrom2D(GetCell2DFromLocationMouse(x, y));
 
        private static int Get1DFrom2D(Point pt) => Get1DFrom2D(pt.X, pt.Y);
 
        private static int Get1DFrom2D(int x, int y)
        {
            if (x == -1 || y == -1)
            {
                return -1;
            }
 
            return x + CellsAcross * y;
        }
 
        private static Point Get2DFrom1D(int cell)
        {
            int x = cell % CellsAcross;
            int y = cell / CellsAcross;
            return new Point(x, y);
        }
 
        private void InvalidateSelection()
        {
            for (int y = 0; y < CellsDown; y++)
            {
                for (int x = 0; x < CellsAcross; x++)
                {
                    if (SelectedColor.Equals(GetColorFromCell(x, y)))
                    {
                        Rectangle r = default;
                        FillRectWithCellBounds(x, y, ref r);
                        Invalidate(Rectangle.Inflate(r, 5, 5));
                        break;
                    }
                }
            }
        }
 
        private void InvalidateFocus()
        {
            Rectangle r = default;
            FillRectWithCellBounds(_focus.X, _focus.Y, ref r);
            Invalidate(Rectangle.Inflate(r, 5, 5));
            PInvoke.NotifyWinEvent(
                (uint)AccessibleEvents.Focus,
                this,
                (int)OBJECT_IDENTIFIER.OBJID_CLIENT,
                1 + Get1DFrom2D(_focus.X, _focus.Y));
        }
 
        protected override bool IsInputKey(Keys keyData) => keyData switch
        {
            Keys.Left or Keys.Right or Keys.Up or Keys.Down or Keys.Enter => true,
            // If we don't do this in ProcessDialogKey, VS will take it from us
            Keys.F2 => false,
            _ => base.IsInputKey(keyData),
        };
 
        protected virtual void LaunchDialog(int customIndex)
        {
            Invalidate();
            _colorUI.EditorService!.CloseDropDown(); // It will be closed anyway as soon as it sees the WM_ACTIVATE
            CustomColorDialog dialog = new();
 
            HWND hwndFocus = PInvoke.GetFocus();
            try
            {
                DialogResult result = dialog.ShowDialog();
                if (result != DialogResult.Cancel)
                {
                    CustomColors[customIndex] = dialog.Color;
                    SelectedColor = CustomColors[customIndex];
                    OnPicked(EventArgs.Empty, Get_onPicked());
                }
 
                dialog.Dispose();
            }
            finally
            {
                if (!hwndFocus.IsNull)
                {
                    PInvoke.SetFocus(hwndFocus);
                }
            }
        }
 
        protected override void OnGotFocus(EventArgs e)
        {
            base.OnGotFocus(e);
            InvalidateFocus();
        }
 
        protected override void OnKeyDown(KeyEventArgs e)
        {
            base.OnKeyDown(e);
            switch (e.KeyCode)
            {
                case Keys.Enter:
                    SelectedColor = GetColorFromCell(_focus.X, _focus.Y);
                    InvalidateFocus();
                    OnPicked(EventArgs.Empty, Get_onPicked());
                    break;
                case Keys.Space:
                    SelectedColor = GetColorFromCell(_focus.X, _focus.Y);
                    InvalidateFocus();
                    break;
                case Keys.Left:
                    SetFocus(_focus with { X = _focus.X - 1 });
                    break;
                case Keys.Right:
                    SetFocus(_focus with { X = _focus.X + 1 });
                    break;
                case Keys.Up:
                    SetFocus(_focus with { Y = _focus.Y - 1 });
                    break;
                case Keys.Down:
                    SetFocus(_focus with { Y = _focus.Y + 1 });
                    break;
            }
        }
 
        protected override void OnLostFocus(EventArgs e)
        {
            base.OnLostFocus(e);
            InvalidateFocus();
        }
 
        protected override void OnMouseDown(MouseEventArgs me)
        {
            base.OnMouseDown(me);
            if (me.Button == MouseButtons.Left)
            {
                Point cell2D = GetCell2DFromLocationMouse(me.X, me.Y);
 
                if (cell2D.X != -1 && cell2D.Y != -1 && cell2D != _focus)
                {
                    SetFocus(cell2D);
                }
            }
        }
 
        protected override void OnMouseMove(MouseEventArgs me)
        {
            base.OnMouseMove(me);
            if (me.Button == MouseButtons.Left && Bounds.Contains(me.X, me.Y))
            {
                Point cell2D = GetCell2DFromLocationMouse(me.X, me.Y);
 
                if (cell2D.X != -1 && cell2D.Y != -1 && cell2D != _focus)
                {
                    SetFocus(cell2D);
                }
            }
        }
 
        protected override void OnMouseUp(MouseEventArgs me)
        {
            base.OnMouseUp(me);
 
            if (me.Button == MouseButtons.Left)
            {
                Point cell2D = GetCell2DFromLocationMouse(me.X, me.Y);
                if (cell2D.X != -1 && cell2D.Y != -1)
                {
                    SetFocus(cell2D);
                    SelectedColor = GetColorFromCell(_focus.X, _focus.Y);
                    OnPicked(EventArgs.Empty, Get_onPicked());
                }
            }
            else if (me.Button == MouseButtons.Right)
            {
                int cell = GetCellFromLocationMouse(me.X, me.Y);
                if (cell is >= TotalCells - CellsCustom and < TotalCells)
                {
                    LaunchDialog(cell - TotalCells + CellsCustom);
                }
            }
        }
 
        protected override void OnPaint(PaintEventArgs pe)
        {
            Graphics graphics = pe.Graphics;
            using var brush = BackColor.GetCachedSolidBrushScope();
            graphics.FillRectangle(brush, ClientRectangle);
 
            Rectangle rect = new()
            {
                Width = s_cellSizeX,
                Height = s_cellSizeY,
                X = s_marginX,
                Y = s_marginY
            };
 
            bool drawSelected = false;
 
            for (int y = 0; y < CellsDown; y++)
            {
                for (int x = 0; x < CellsAcross; x++)
                {
                    Color color = GetColorFromCell(Get1DFrom2D(x, y));
 
                    FillRectWithCellBounds(x, y, ref rect);
 
                    if (color.Equals(SelectedColor) && !drawSelected)
                    {
                        ControlPaint.DrawBorder(
                            graphics,
                            Rectangle.Inflate(rect, 3, 3),
                            SystemColors.ControlText,
                            ButtonBorderStyle.Solid);
 
                        drawSelected = true;
                    }
 
                    if (_focus.X == x && _focus.Y == y && Focused)
                    {
                        ControlPaint.DrawFocusRectangle(
                            graphics,
                            Rectangle.Inflate(rect, 5, 5),
                            SystemColors.ControlText,
                            SystemColors.Control);
                    }
 
                    ControlPaint.DrawBorder(
                        graphics,
                        Rectangle.Inflate(rect, 2, 2),
                        SystemColors.Control, 2, ButtonBorderStyle.Inset,
                        SystemColors.Control, 2, ButtonBorderStyle.Inset,
                        SystemColors.Control, 2, ButtonBorderStyle.Inset,
                        SystemColors.Control, 2, ButtonBorderStyle.Inset);
 
                    PaintValue(color, graphics, rect);
                }
            }
        }
 
        private static void PaintValue(Color color, Graphics g, Rectangle rect)
        {
            using var brush = color.GetCachedSolidBrushScope();
            g.FillRectangle(brush, rect);
        }
 
        protected override bool ProcessDialogKey(Keys keyData)
        {
            if (keyData == Keys.F2)
            {
                // No ctrl, alt, shift.
                int cell = Get1DFrom2D(_focus.X, _focus.Y);
                if (cell is >= TotalCells - CellsCustom and < TotalCells)
                {
                    LaunchDialog(cell - TotalCells + CellsCustom);
                    return true;
                }
            }
 
            return base.ProcessDialogKey(keyData);
        }
 
        private void SetFocus(Point newFocus)
        {
            // Make sure newFocus is within correct range of cells
            if (newFocus.X < 0)
            {
                newFocus.X = 0;
            }
 
            if (newFocus.Y < 0)
            {
                newFocus.Y = 0;
            }
 
            if (newFocus.X >= CellsAcross)
            {
                newFocus.X = CellsAcross - 1;
            }
 
            if (newFocus.Y >= CellsDown)
            {
                newFocus.Y = CellsDown - 1;
            }
 
            if (_focus != newFocus)
            {
                InvalidateFocus();
                _focus = newFocus;
                InvalidateFocus();
            }
        }
    }
}