Example #1
0
        public static Rgb AverageColor(BitmapSource source)
        {
            if (source.Format != PixelFormats.Bgra32)
            {
                throw new Exception("Unexpected Bitmap format.");
            }

            var width         = source.PixelWidth;
            var height        = source.PixelHeight;
            var numPixels     = width * height;
            var bytesPerPixel = source.Format.BitsPerPixel / 8;
            var pixelBuffer   = new byte[numPixels * bytesPerPixel];

            source.CopyPixels(pixelBuffer, width * bytesPerPixel, 0);

            long blue  = 0;
            long green = 0;
            long red   = 0;

            for (var i = 0; i < pixelBuffer.Length; i += bytesPerPixel)
            {
                blue  += pixelBuffer[i];
                green += pixelBuffer[i + 1];
                red   += pixelBuffer[i + 2];
            }

            return(Rgb.FromScaledValues((byte)(red / numPixels), (byte)(green / numPixels), (byte)(blue / numPixels)));
        }
Example #2
0
        public static Rgb PixelToRgb(BitmapSource source, int x, int y)
        {
            if (source.Format != PixelFormats.Bgra32)
            {
                throw new Exception("Unexpected Bitmap format.");
            }

            var bytesPerPixel = source.Format.BitsPerPixel / 8;
            var bytes         = new byte[bytesPerPixel];
            var rect          = new Int32Rect(x, y, 1, 1);

            source.CopyPixels(rect, bytes, bytesPerPixel, 0);

            return(Rgb.FromScaledValues(bytes[2], bytes[1], bytes[0]));
        }
Example #3
0
        public void NewRgb_WithOutOfRangeValue_ShouldThrowException()
        {
            Assert.ThrowsException <ArgumentException>(() => new Rgb(-0.1, 0, 0));
            Assert.ThrowsException <ArgumentException>(() => new Rgb(1.1, 0, 0));
            Assert.ThrowsException <ArgumentException>(() => Rgb.FromScaledValues(-1, 0, 0));
            Assert.ThrowsException <ArgumentException>(() => Rgb.FromScaledValues(256, 0, 0));

            Assert.ThrowsException <ArgumentException>(() => new Rgb(0, -0.1, 0));
            Assert.ThrowsException <ArgumentException>(() => new Rgb(0, 1.1, 0));
            Assert.ThrowsException <ArgumentException>(() => Rgb.FromScaledValues(0, -1, 0));
            Assert.ThrowsException <ArgumentException>(() => Rgb.FromScaledValues(0, 256, 0));

            Assert.ThrowsException <ArgumentException>(() => new Rgb(0, 0, -0.1));
            Assert.ThrowsException <ArgumentException>(() => new Rgb(0, 0, 1.1));
            Assert.ThrowsException <ArgumentException>(() => Rgb.FromScaledValues(0, 0, -1));
            Assert.ThrowsException <ArgumentException>(() => Rgb.FromScaledValues(0, 0, 256));
        }
        // Conversion tool for reference:
        // https://www.easyrgb.com/en/convert.php#inputFORM

        public IEnumerable <object[]> GetData(MethodInfo methodInfo)
        {
            yield return(new object[]
            {
                new ColorData
                {
                    Name = "Black",
                    Rgb = new Rgb(0, 0, 0),
                    Hsl = new Hsl(0, 0, 0),
                    Hsv = new Hsv(0, 0, 0),
                    Cmyk = new Cmyk(0, 0, 0, 1),
                    Xyz = new Xyz(0, 0, 0),
                    Lab = new Lab(0, 0, 0)
                }
            });

            yield return(new object[]
            {
                new ColorData
                {
                    Name = "White",
                    Rgb = new Rgb(1, 1, 1),
                    Hsl = new Hsl(0, 0, 1),
                    Hsv = new Hsv(0, 0, 1),
                    Cmyk = new Cmyk(0, 0, 0, 0),
                    Xyz = new Xyz(95.047, 100, 108.883),
                    Lab = new Lab(100, 0, 0)
                }
            });

            yield return(new object[]
            {
                new ColorData
                {
                    Name = "Red",
                    Rgb = new Rgb(1, 0, 0),
                    Hsl = new Hsl(0, 1, 0.5),
                    Hsv = new Hsv(0, 1, 1),
                    Cmyk = new Cmyk(0, 1, 1, 0),
                    Xyz = new Xyz(41.2456, 21.2673, 1.9334),
                    Lab = new Lab(53.2408, 80.0925, 67.2032)
                }
            });

            yield return(new object[]
            {
                new ColorData
                {
                    Name = "Green",
                    Rgb = new Rgb(0, 1, 0),
                    Hsl = Hsl.FromScaledValues(120, 100, 50),
                    Hsv = Hsv.FromScaledValues(120, 100, 100),
                    Cmyk = new Cmyk(1, 0, 1, 0),
                    Xyz = new Xyz(35.7576, 71.5152, 11.9192),
                    Lab = new Lab(87.7347, -86.1827, 83.1793)
                }
            });

            yield return(new object[]
            {
                new ColorData
                {
                    Name = "Blue",
                    Rgb = new Rgb(0, 0, 1),
                    Hsl = Hsl.FromScaledValues(240, 100, 50),
                    Hsv = Hsv.FromScaledValues(240, 100, 100),
                    Cmyk = new Cmyk(1, 1, 0, 0),
                    Xyz = new Xyz(18.0437, 7.2175, 95.0304),
                    Lab = new Lab(32.297, 79.1875, -107.8602)
                }
            });

            yield return(new object[]
            {
                new ColorData
                {
                    Name = "Material Blue",
                    Rgb = Rgb.FromScaledValues(33, 150, 243),
                    Hsl = Hsl.FromScaledValues(206.5716, 89.744, 54.118),
                    Hsv = Hsv.FromScaledValues(206.5716, 86.42, 95.294),
                    Cmyk = Cmyk.FromScaledValues(86.419753, 38.271604, 0, 4.705882),
                    Xyz = new Xyz(27.704960365063513, 28.60350077011483, 88.83745176406208),
                    Lab = new Lab(60.4301, 2.0799, -55.1094)
                }
            });

            yield return(new object[]
            {
                new ColorData
                {
                    Name = "Purple HEX to HSL/HSV issue",
                    Rgb = Rgb.FromScaledValues(116, 58, 111),
                    Hsl = Hsl.FromScaledValues(305.172, 33.333, 34.118),
                    Hsv = Hsv.FromScaledValues(305.172, 50, 45.49),
                    Cmyk = Cmyk.FromScaledValues(0, 50, 4.310, 54.510),
                    Xyz = new Xyz(11.5846, 7.8875, 15.9481),
                    Lab = new Lab(33.7475, 33.4761, -19.6542)
                }
            });
        }
Example #5
0
        public MainWindow()
        {
            InitializeComponent();
            Style = (Style)FindResource(typeof(System.Windows.Window));
            _vm   = new MainWindowViewModel();
            _vm.LoadFromPersistedData(PersistedState.Data, ColorModelTabs);
            DataContext = _vm;

            if (_vm.Rgb == Rgb.Empty)
            {
                // Material Blue – #2196F3
                _vm.RefreshFromRgb(Rgb.FromScaledValues(33, 150, 243));
            }

            // Eyedropper
            EyedropperButton.Click += (o, ev) => { StartEyedropper(); };

            // Lighter / Darker Buttons
            LighterButton.Click += (o, ev) => { _vm.RefreshFromHsl(_vm.Hsl.Lighter(5)); };
            DarkerButton.Click  += (o, ev) => { _vm.RefreshFromHsl(_vm.Hsl.Darker(5)); };

            // Color model tabs
            _vm.PropertyChanged += (o, ev) =>
            {
                if (ev.PropertyName == "ActiveColorModelTabItem")
                {
                    _vm.SaveToPersistedData(PersistedState.Data);
                }
            };

            // Helper function to reduce clipboard button boilerplate:
            void SetupClipboardButton(Button button, Func <string> valueGetter)
            {
                EventUtil.HandleClick(button, (sender, e) =>
                {
                    // SetDataObject seems to work better than SetText or other methods, when programs like RealVNC are locking up the clipboard.
                    Clipboard.SetDataObject(valueGetter());
                    DisplayClipboardCheckmark((Button)sender);
                });
            }

            //
            // RGB fields
            //

            EventUtil.HandleKey(Key.Up, RgbRed, () => { _vm.RefreshFromRgb(_vm.Rgb.WithRed(_vm.Rgb.Red + 0.01)); });
            EventUtil.HandleKey(Key.Up, RgbGreen, () => { _vm.RefreshFromRgb(_vm.Rgb.WithGreen(_vm.Rgb.Green + 0.01)); });
            EventUtil.HandleKey(Key.Up, RgbBlue, () => { _vm.RefreshFromRgb(_vm.Rgb.WithBlue(_vm.Rgb.Blue + 0.01)); });
            EventUtil.HandleKey(Key.Up, RgbScaledRed, () => { _vm.RefreshFromRgb(_vm.Rgb.WithRed(_vm.Rgb.ScaledRed + 1)); });
            EventUtil.HandleKey(Key.Up, RgbScaledGreen, () => { _vm.RefreshFromRgb(_vm.Rgb.WithGreen(_vm.Rgb.ScaledGreen + 1)); });
            EventUtil.HandleKey(Key.Up, RgbScaledBlue, () => { _vm.RefreshFromRgb(_vm.Rgb.WithBlue(_vm.Rgb.ScaledBlue + 1)); });

            EventUtil.HandleKey(Key.Down, RgbRed, () => { _vm.RefreshFromRgb(_vm.Rgb.WithRed(_vm.Rgb.Red - 0.01)); });
            EventUtil.HandleKey(Key.Down, RgbGreen, () => { _vm.RefreshFromRgb(_vm.Rgb.WithGreen(_vm.Rgb.Green - 0.01)); });
            EventUtil.HandleKey(Key.Down, RgbBlue, () => { _vm.RefreshFromRgb(_vm.Rgb.WithBlue(_vm.Rgb.Blue - 0.01)); });
            EventUtil.HandleKey(Key.Down, RgbScaledRed, () => { _vm.RefreshFromRgb(_vm.Rgb.WithRed(_vm.Rgb.ScaledRed - 1)); });
            EventUtil.HandleKey(Key.Down, RgbScaledGreen, () => { _vm.RefreshFromRgb(_vm.Rgb.WithGreen(_vm.Rgb.ScaledGreen - 1)); });
            EventUtil.HandleKey(Key.Down, RgbScaledBlue, () => { _vm.RefreshFromRgb(_vm.Rgb.WithBlue(_vm.Rgb.ScaledBlue - 1)); });

            EventUtil.HandleMouseWheel(
                RgbRed,
                () => { _vm.RefreshFromRgb(_vm.Rgb.WithRed(_vm.Rgb.Red + 0.01)); },
                () => { _vm.RefreshFromRgb(_vm.Rgb.WithRed(_vm.Rgb.Red - 0.01)); }
                );
            EventUtil.HandleMouseWheel(
                RgbGreen,
                () => { _vm.RefreshFromRgb(_vm.Rgb.WithGreen(_vm.Rgb.Green + 0.01)); },
                () => { _vm.RefreshFromRgb(_vm.Rgb.WithGreen(_vm.Rgb.Green - 0.01)); }
                );
            EventUtil.HandleMouseWheel(
                RgbBlue,
                () => { _vm.RefreshFromRgb(_vm.Rgb.WithBlue(_vm.Rgb.Blue + 0.01)); },
                () => { _vm.RefreshFromRgb(_vm.Rgb.WithBlue(_vm.Rgb.Blue - 0.01)); }
                );

            EventUtil.HandleMouseWheel(
                RgbScaledRed,
                () => { _vm.RefreshFromRgb(_vm.Rgb.WithRed(_vm.Rgb.ScaledRed + 1)); },
                () => { _vm.RefreshFromRgb(_vm.Rgb.WithRed(_vm.Rgb.ScaledRed - 1)); }
                );
            EventUtil.HandleMouseWheel(
                RgbScaledGreen,
                () => { _vm.RefreshFromRgb(_vm.Rgb.WithGreen(_vm.Rgb.ScaledGreen + 1)); },
                () => { _vm.RefreshFromRgb(_vm.Rgb.WithGreen(_vm.Rgb.ScaledGreen - 1)); }
                );
            EventUtil.HandleMouseWheel(
                RgbScaledBlue,
                () => { _vm.RefreshFromRgb(_vm.Rgb.WithBlue(_vm.Rgb.ScaledBlue + 1)); },
                () => { _vm.RefreshFromRgb(_vm.Rgb.WithBlue(_vm.Rgb.ScaledBlue - 1)); }
                );

            EventUtil.HandleSliderChange(RgbRedSlider, value => { _vm.RefreshFromRgb(_vm.Rgb.WithRed(value)); });
            EventUtil.HandleSliderChange(RgbGreenSlider, value => { _vm.RefreshFromRgb(_vm.Rgb.WithGreen(value)); });
            EventUtil.HandleSliderChange(RgbBlueSlider, value => { _vm.RefreshFromRgb(_vm.Rgb.WithBlue(value)); });

            _vm.PropertyChangedByUser += (o, ev) =>
            {
                int    intVal;
                bool   isInt;
                bool   isDouble;
                double doubleVal;
                Rgb?   nullableRgb;

                switch (ev.PropertyName)
                {
                case nameof(MainWindowViewModel.RgbText):
                    nullableRgb = Rgb.FromString(_vm.RgbText);
                    if (nullableRgb.HasValue)
                    {
                        _vm.RefreshFromRgb(nullableRgb.Value);
                    }
                    break;

                case nameof(MainWindowViewModel.RgbScaledText):
                    nullableRgb = Rgb.FromScaledString(_vm.RgbScaledText);
                    if (nullableRgb.HasValue)
                    {
                        _vm.RefreshFromRgb(nullableRgb.Value);
                    }
                    break;

                case nameof(MainWindowViewModel.RgbScaledRed):
                    isInt = int.TryParse(_vm.RgbScaledRed, out intVal);
                    if (!isInt || !Rgb.IsValidScaledComponent(intVal))
                    {
                        return;
                    }
                    _vm.RefreshFromRgb(_vm.Rgb.WithRed(intVal));
                    break;

                case nameof(MainWindowViewModel.RgbScaledGreen):
                    isInt = int.TryParse(_vm.RgbScaledGreen, out intVal);
                    if (!isInt || !Rgb.IsValidScaledComponent(intVal))
                    {
                        return;
                    }
                    _vm.RefreshFromRgb(_vm.Rgb.WithGreen(intVal));
                    break;

                case nameof(MainWindowViewModel.RgbScaledBlue):
                    isInt = int.TryParse(_vm.RgbScaledBlue, out intVal);
                    if (!isInt || !Rgb.IsValidScaledComponent(intVal))
                    {
                        return;
                    }
                    _vm.RefreshFromRgb(_vm.Rgb.WithBlue(intVal));
                    break;

                case nameof(MainWindowViewModel.RgbRed):
                    isDouble = double.TryParse(_vm.RgbRed, out doubleVal);
                    if (!isDouble || !Rgb.IsValidComponent(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromRgb(_vm.Rgb.WithRed(doubleVal));
                    break;

                case nameof(MainWindowViewModel.RgbGreen):
                    isDouble = double.TryParse(_vm.RgbGreen, out doubleVal);
                    if (!isDouble || !Rgb.IsValidComponent(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromRgb(_vm.Rgb.WithGreen(doubleVal));
                    break;

                case nameof(MainWindowViewModel.RgbBlue):
                    isDouble = double.TryParse(_vm.RgbBlue, out doubleVal);
                    if (!isDouble || !Rgb.IsValidComponent(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromRgb(_vm.Rgb.WithBlue(doubleVal));
                    break;
                }
            };

            SetupClipboardButton(RgbScaledTextClipboardButton, () => _vm.RgbScaledText);
            SetupClipboardButton(RgbTextClipboardButton, () => _vm.RgbText);

            //
            // HEX fields
            //

            EventUtil.HandleKey(Key.Up, HexRed, () => { _vm.RefreshFromHex(_vm.Hex.WithRed(_vm.Hex.Red + 1)); });
            EventUtil.HandleKey(Key.Up, HexGreen, () => { _vm.RefreshFromHex(_vm.Hex.WithGreen(_vm.Hex.Green + 1)); });
            EventUtil.HandleKey(Key.Up, HexBlue, () => { _vm.RefreshFromHex(_vm.Hex.WithBlue(_vm.Hex.Blue + 1)); });

            EventUtil.HandleKey(Key.Down, HexRed, () => { _vm.RefreshFromHex(_vm.Hex.WithRed(_vm.Hex.Red - 1)); });
            EventUtil.HandleKey(Key.Down, HexGreen, () => { _vm.RefreshFromHex(_vm.Hex.WithGreen(_vm.Hex.Green - 1)); });
            EventUtil.HandleKey(Key.Down, HexBlue, () => { _vm.RefreshFromHex(_vm.Hex.WithBlue(_vm.Hex.Blue - 1)); });

            EventUtil.HandleMouseWheel(
                HexRed,
                () => { _vm.RefreshFromHex(_vm.Hex.WithRed(_vm.Hex.Red + 1)); },
                () => { _vm.RefreshFromHex(_vm.Hex.WithRed(_vm.Hex.Red - 1)); }
                );
            EventUtil.HandleMouseWheel(
                HexGreen,
                () => { _vm.RefreshFromHex(_vm.Hex.WithGreen(_vm.Hex.Green + 1)); },
                () => { _vm.RefreshFromHex(_vm.Hex.WithGreen(_vm.Hex.Green - 1)); }
                );
            EventUtil.HandleMouseWheel(
                HexBlue,
                () => { _vm.RefreshFromHex(_vm.Hex.WithBlue(_vm.Hex.Blue + 1)); },
                () => { _vm.RefreshFromHex(_vm.Hex.WithBlue(_vm.Hex.Blue - 1)); }
                );

            EventUtil.HandleSliderChange(HexRedSlider, value => { _vm.RefreshFromRgb(_vm.Rgb.WithRed(value)); });
            EventUtil.HandleSliderChange(HexGreenSlider, value => { _vm.RefreshFromRgb(_vm.Rgb.WithGreen(value)); });
            EventUtil.HandleSliderChange(HexBlueSlider, value => { _vm.RefreshFromRgb(_vm.Rgb.WithBlue(value)); });

            // The box actively ignores 3-char hex strings while typing so that 6-chars may be entered without interruption.
            // However, 3-char hex strings can still be entered by pressing Enter or dropping focus:
            EventUtil.HandleInputEnterOrFocusLost(HexText, value =>
            {
                var nullableHex = Hex.FromString(_vm.HexText);
                if (nullableHex.HasValue)
                {
                    _vm.RefreshFromHex(nullableHex.Value);
                }
            });

            _vm.PropertyChangedByUser += (o, ev) =>
            {
                switch (ev.PropertyName)
                {
                case nameof(MainWindowViewModel.HexText):
                    var nullableHex = Hex.From6CharString(_vm.HexText);
                    if (nullableHex.HasValue)
                    {
                        _vm.RefreshFromHex(nullableHex.Value);
                    }
                    break;

                case nameof(MainWindowViewModel.HexRed):
                    // Length comparison for better UX. Stops converting "0" to "00" and moving user cursor.
                    if (!Hex.IsValidHexPart(_vm.HexRed) || _vm.HexRed.Length != 2)
                    {
                        return;
                    }
                    _vm.RefreshFromHex(_vm.Hex.WithRed(_vm.HexRed));
                    break;

                case nameof(MainWindowViewModel.HexGreen):
                    // Length comparison for better UX. Stops converting "0" to "00" and moving user cursor.
                    if (!Hex.IsValidHexPart(_vm.HexGreen) || _vm.HexGreen.Length != 2)
                    {
                        return;
                    }
                    _vm.RefreshFromHex(_vm.Hex.WithGreen(_vm.HexGreen));
                    break;

                case nameof(MainWindowViewModel.HexBlue):
                    // Length comparison for better UX. Stops converting "0" to "00" and moving user cursor.
                    if (!Hex.IsValidHexPart(_vm.HexBlue) || _vm.HexBlue.Length != 2)
                    {
                        return;
                    }
                    _vm.RefreshFromHex(_vm.Hex.WithBlue(_vm.HexBlue));
                    break;
                }
            };

            SetupClipboardButton(HexTextClipboardButton, () => _vm.HexText);

            //
            // HSL fields
            //

            EventUtil.HandleKey(Key.Up, HslHue, () => { _vm.RefreshFromHsl(_vm.Hsl.WithHue(_vm.Hsl.Hue + 0.01)); });
            EventUtil.HandleKey(Key.Up, HslSaturation, () => { _vm.RefreshFromHsl(_vm.Hsl.WithSaturation(_vm.Hsl.Saturation + 0.01)); });
            EventUtil.HandleKey(Key.Up, HslLuminance, () => { _vm.RefreshFromHsl(_vm.Hsl.WithLuminance(_vm.Hsl.Luminance + 0.01)); });
            EventUtil.HandleKey(Key.Up, HslScaledHue, () => { _vm.RefreshFromHsl(_vm.Hsl.WithScaledHue(_vm.Hsl.ScaledHue + 1)); });
            EventUtil.HandleKey(Key.Up, HslScaledSaturation, () => { _vm.RefreshFromHsl(_vm.Hsl.WithScaledSaturation(_vm.Hsl.ScaledSaturation + 1)); });
            EventUtil.HandleKey(Key.Up, HslScaledLuminance, () => { _vm.RefreshFromHsl(_vm.Hsl.WithScaledLuminance(_vm.Hsl.ScaledLuminance + 1)); });

            EventUtil.HandleKey(Key.Down, HslHue, () => { _vm.RefreshFromHsl(_vm.Hsl.WithHue(_vm.Hsl.Hue - 0.01)); });
            EventUtil.HandleKey(Key.Down, HslSaturation, () => { _vm.RefreshFromHsl(_vm.Hsl.WithSaturation(_vm.Hsl.Saturation - 0.01)); });
            EventUtil.HandleKey(Key.Down, HslLuminance, () => { _vm.RefreshFromHsl(_vm.Hsl.WithLuminance(_vm.Hsl.Luminance - 0.01)); });
            EventUtil.HandleKey(Key.Down, HslScaledHue, () => { _vm.RefreshFromHsl(_vm.Hsl.WithScaledHue(_vm.Hsl.ScaledHue - 1)); });
            EventUtil.HandleKey(Key.Down, HslScaledSaturation, () => { _vm.RefreshFromHsl(_vm.Hsl.WithScaledSaturation(_vm.Hsl.ScaledSaturation - 1)); });
            EventUtil.HandleKey(Key.Down, HslScaledLuminance, () => { _vm.RefreshFromHsl(_vm.Hsl.WithScaledLuminance(_vm.Hsl.ScaledLuminance - 1)); });

            EventUtil.HandleMouseWheel(
                HslHue,
                () => { _vm.RefreshFromHsl(_vm.Hsl.WithHue(_vm.Hsl.Hue + 0.01)); },
                () => { _vm.RefreshFromHsl(_vm.Hsl.WithHue(_vm.Hsl.Hue - 0.01)); }
                );
            EventUtil.HandleMouseWheel(
                HslSaturation,
                () => { _vm.RefreshFromHsl(_vm.Hsl.WithSaturation(_vm.Hsl.Saturation + 0.01)); },
                () => { _vm.RefreshFromHsl(_vm.Hsl.WithSaturation(_vm.Hsl.Saturation - 0.01)); }
                );
            EventUtil.HandleMouseWheel(
                HslLuminance,
                () => { _vm.RefreshFromHsl(_vm.Hsl.WithLuminance(_vm.Hsl.Luminance + 0.01)); },
                () => { _vm.RefreshFromHsl(_vm.Hsl.WithLuminance(_vm.Hsl.Luminance - 0.01)); }
                );
            EventUtil.HandleMouseWheel(
                HslScaledHue,
                () => { _vm.RefreshFromHsl(_vm.Hsl.WithScaledHue(_vm.Hsl.ScaledHue + 1)); },
                () => { _vm.RefreshFromHsl(_vm.Hsl.WithScaledHue(_vm.Hsl.ScaledHue - 1)); }
                );
            EventUtil.HandleMouseWheel(
                HslScaledSaturation,
                () => { _vm.RefreshFromHsl(_vm.Hsl.WithScaledSaturation(_vm.Hsl.ScaledSaturation + 1)); },
                () => { _vm.RefreshFromHsl(_vm.Hsl.WithScaledSaturation(_vm.Hsl.ScaledSaturation - 1)); }
                );
            EventUtil.HandleMouseWheel(
                HslScaledLuminance,
                () => { _vm.RefreshFromHsl(_vm.Hsl.WithScaledLuminance(_vm.Hsl.ScaledLuminance + 1)); },
                () => { _vm.RefreshFromHsl(_vm.Hsl.WithScaledLuminance(_vm.Hsl.ScaledLuminance - 1)); }
                );

            EventUtil.HandleSliderChange(HslHueSlider, value => { _vm.RefreshFromHsl(_vm.Hsl.WithHue(value)); });
            EventUtil.HandleSliderChange(HslSaturationSlider, value => { _vm.RefreshFromHsl(_vm.Hsl.WithSaturation(value)); });
            EventUtil.HandleSliderChange(HslLuminanceSlider, value => { _vm.RefreshFromHsl(_vm.Hsl.WithLuminance(value)); });

            _vm.PropertyChangedByUser += (o, ev) =>
            {
                bool   isDouble;
                double doubleVal;
                Hsl?   nullableHsl;

                switch (ev.PropertyName)
                {
                case nameof(MainWindowViewModel.HslText):
                    nullableHsl = Hsl.FromString(_vm.HslText);
                    if (nullableHsl.HasValue)
                    {
                        _vm.RefreshFromHsl(nullableHsl.Value);
                    }
                    break;

                case nameof(MainWindowViewModel.HslScaledText):
                    nullableHsl = Hsl.FromScaledString(_vm.HslScaledText);
                    if (nullableHsl.HasValue)
                    {
                        _vm.RefreshFromHsl(nullableHsl.Value);
                    }
                    break;

                case nameof(MainWindowViewModel.HslHue):
                    isDouble = double.TryParse(_vm.HslHue, out doubleVal);
                    if (!isDouble || !Hsl.IsValidHue(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromHsl(_vm.Hsl.WithHue(doubleVal));
                    break;

                case nameof(MainWindowViewModel.HslSaturation):
                    isDouble = double.TryParse(_vm.HslSaturation, out doubleVal);
                    if (!isDouble || !Hsl.IsValidSaturation(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromHsl(_vm.Hsl.WithSaturation(doubleVal));
                    break;

                case nameof(MainWindowViewModel.HslLuminance):
                    isDouble = double.TryParse(_vm.HslLuminance, out doubleVal);
                    if (!isDouble || !Hsl.IsValidLuminance(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromHsl(_vm.Hsl.WithLuminance(doubleVal));
                    break;

                case nameof(MainWindowViewModel.HslScaledHue):
                    isDouble = double.TryParse(_vm.HslScaledHue, out doubleVal);
                    if (!isDouble || !Hsl.IsValidScaledHue(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromHsl(_vm.Hsl.WithScaledHue(doubleVal));
                    break;

                case nameof(MainWindowViewModel.HslScaledSaturation):
                    isDouble = double.TryParse(_vm.HslScaledHue, out doubleVal);
                    if (!isDouble || !Hsl.IsValidScaledSaturation(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromHsl(_vm.Hsl.WithScaledSaturation(doubleVal));
                    break;

                case nameof(MainWindowViewModel.HslScaledLuminance):
                    isDouble = double.TryParse(_vm.HslScaledHue, out doubleVal);
                    if (!isDouble || !Hsl.IsValidScaledLuminance(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromHsl(_vm.Hsl.WithScaledLuminance(doubleVal));
                    break;
                }
            };

            SetupClipboardButton(HslScaledTextClipboardButton, () => _vm.HslScaledText);
            SetupClipboardButton(HslTextClipboardButton, () => _vm.HslText);

            //
            // HSV fields
            //

            EventUtil.HandleKey(Key.Up, HsvHue, () => { _vm.RefreshFromHsv(_vm.Hsv.WithHue(_vm.Hsv.Hue + 0.01)); });
            EventUtil.HandleKey(Key.Up, HsvSaturation, () => { _vm.RefreshFromHsv(_vm.Hsv.WithSaturation(_vm.Hsv.Saturation + 0.01)); });
            EventUtil.HandleKey(Key.Up, HsvValue, () => { _vm.RefreshFromHsv(_vm.Hsv.WithValue(_vm.Hsv.Value + 0.01)); });
            EventUtil.HandleKey(Key.Up, HsvScaledHue, () => { _vm.RefreshFromHsv(_vm.Hsv.WithScaledHue(_vm.Hsv.ScaledHue + 1)); });
            EventUtil.HandleKey(Key.Up, HsvScaledSaturation, () => { _vm.RefreshFromHsv(_vm.Hsv.WithScaledSaturation(_vm.Hsv.ScaledSaturation + 1)); });
            EventUtil.HandleKey(Key.Up, HsvScaledValue, () => { _vm.RefreshFromHsv(_vm.Hsv.WithScaledValue(_vm.Hsv.ScaledValue + 1)); });

            EventUtil.HandleKey(Key.Down, HsvHue, () => { _vm.RefreshFromHsv(_vm.Hsv.WithHue(_vm.Hsv.Hue - 0.01)); });
            EventUtil.HandleKey(Key.Down, HsvSaturation, () => { _vm.RefreshFromHsv(_vm.Hsv.WithSaturation(_vm.Hsv.Saturation - 0.01)); });
            EventUtil.HandleKey(Key.Down, HsvValue, () => { _vm.RefreshFromHsv(_vm.Hsv.WithValue(_vm.Hsv.Value - 0.01)); });
            EventUtil.HandleKey(Key.Down, HsvScaledHue, () => { _vm.RefreshFromHsv(_vm.Hsv.WithScaledHue(_vm.Hsv.ScaledHue - 1)); });
            EventUtil.HandleKey(Key.Down, HsvScaledSaturation, () => { _vm.RefreshFromHsv(_vm.Hsv.WithScaledSaturation(_vm.Hsv.ScaledSaturation - 1)); });
            EventUtil.HandleKey(Key.Down, HsvScaledValue, () => { _vm.RefreshFromHsv(_vm.Hsv.WithScaledValue(_vm.Hsv.ScaledValue - 1)); });

            EventUtil.HandleMouseWheel(HsvHue, () => { _vm.RefreshFromHsv(_vm.Hsv.WithHue(_vm.Hsv.Hue + 0.01)); },
                                       () => { _vm.RefreshFromHsv(_vm.Hsv.WithHue(_vm.Hsv.Hue - 0.01)); });
            EventUtil.HandleMouseWheel(HsvSaturation, () => { _vm.RefreshFromHsv(_vm.Hsv.WithSaturation(_vm.Hsv.Saturation + 0.01)); },
                                       () => { _vm.RefreshFromHsv(_vm.Hsv.WithSaturation(_vm.Hsv.Saturation - 0.01)); });
            EventUtil.HandleMouseWheel(HsvValue, () => { _vm.RefreshFromHsv(_vm.Hsv.WithValue(_vm.Hsv.Value + 0.01)); },
                                       () => { _vm.RefreshFromHsv(_vm.Hsv.WithValue(_vm.Hsv.Value - 0.01)); });
            EventUtil.HandleMouseWheel(HsvScaledHue, () => { _vm.RefreshFromHsv(_vm.Hsv.WithScaledHue(_vm.Hsv.ScaledHue + 1)); },
                                       () => { _vm.RefreshFromHsv(_vm.Hsv.WithScaledHue(_vm.Hsv.ScaledHue - 1)); });
            EventUtil.HandleMouseWheel(HsvScaledSaturation, () => { _vm.RefreshFromHsv(_vm.Hsv.WithScaledSaturation(_vm.Hsv.ScaledSaturation + 1)); },
                                       () => { _vm.RefreshFromHsv(_vm.Hsv.WithScaledSaturation(_vm.Hsv.ScaledSaturation - 1)); });
            EventUtil.HandleMouseWheel(HsvScaledValue, () => { _vm.RefreshFromHsv(_vm.Hsv.WithScaledValue(_vm.Hsv.ScaledValue + 1)); },
                                       () => { _vm.RefreshFromHsv(_vm.Hsv.WithScaledValue(_vm.Hsv.ScaledValue - 1)); });

            EventUtil.HandleSliderChange(HsvHueSlider, value => { _vm.RefreshFromHsv(_vm.Hsv.WithHue(value)); });
            EventUtil.HandleSliderChange(HsvSaturationSlider, value => { _vm.RefreshFromHsv(_vm.Hsv.WithSaturation(value)); });
            EventUtil.HandleSliderChange(HsvValueSlider, value => { _vm.RefreshFromHsv(_vm.Hsv.WithValue(value)); });

            _vm.PropertyChangedByUser += (o, ev) =>
            {
                bool   isDouble;
                double doubleVal;
                Hsv?   nullableHsv;

                switch (ev.PropertyName)
                {
                case nameof(MainWindowViewModel.HsvText):
                    nullableHsv = Hsv.FromString(_vm.HsvText);
                    if (nullableHsv.HasValue)
                    {
                        _vm.RefreshFromHsv(nullableHsv.Value);
                    }
                    break;

                case nameof(MainWindowViewModel.HsvScaledText):
                    nullableHsv = Hsv.FromScaledString(_vm.HsvScaledText);
                    if (nullableHsv.HasValue)
                    {
                        _vm.RefreshFromHsv(nullableHsv.Value);
                    }
                    break;

                case nameof(MainWindowViewModel.HsvHue):
                    isDouble = double.TryParse(_vm.HsvHue, out doubleVal);
                    if (!isDouble || !Hsv.IsValidHue(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromHsv(_vm.Hsv.WithHue(doubleVal));
                    break;

                case nameof(MainWindowViewModel.HsvSaturation):
                    isDouble = double.TryParse(_vm.HsvSaturation, out doubleVal);
                    if (!isDouble || !Hsv.IsValidSaturation(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromHsv(_vm.Hsv.WithSaturation(doubleVal));
                    break;

                case nameof(MainWindowViewModel.HsvValue):
                    isDouble = double.TryParse(_vm.HsvValue, out doubleVal);
                    if (!isDouble || !Hsv.IsValidValue(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromHsv(_vm.Hsv.WithValue(doubleVal));
                    break;

                case nameof(MainWindowViewModel.HsvScaledHue):
                    isDouble = double.TryParse(_vm.HsvScaledHue, out doubleVal);
                    if (!isDouble || !Hsv.IsValidScaledHue(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromHsv(_vm.Hsv.WithScaledHue(doubleVal));
                    break;

                case nameof(MainWindowViewModel.HsvScaledSaturation):
                    isDouble = double.TryParse(_vm.HsvScaledHue, out doubleVal);
                    if (!isDouble || !Hsv.IsValidScaledSaturation(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromHsv(_vm.Hsv.WithScaledSaturation(doubleVal));
                    break;

                case nameof(MainWindowViewModel.HsvScaledValue):
                    isDouble = double.TryParse(_vm.HsvScaledHue, out doubleVal);
                    if (!isDouble || !Hsv.IsValidScaledValue(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromHsv(_vm.Hsv.WithScaledValue(doubleVal));
                    break;
                }
            };

            SetupClipboardButton(HsvScaledTextClipboardButton, () => _vm.HsvScaledText);
            SetupClipboardButton(HsvTextClipboardButton, () => _vm.HsvText);

            //
            // CMYK fields
            //

            EventUtil.HandleKey(Key.Up, CmykCyan, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithCyan(_vm.Cmyk.Cyan + 0.01)); });
            EventUtil.HandleKey(Key.Up, CmykMagenta, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithMagenta(_vm.Cmyk.Magenta + 0.01)); });
            EventUtil.HandleKey(Key.Up, CmykYellow, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithYellow(_vm.Cmyk.Yellow + 0.01)); });
            EventUtil.HandleKey(Key.Up, CmykKey, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithKey(_vm.Cmyk.Key + 0.01)); });
            EventUtil.HandleKey(Key.Up, CmykScaledCyan, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledCyan(_vm.Cmyk.ScaledCyan + 1)); });
            EventUtil.HandleKey(Key.Up, CmykScaledMagenta, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledMagenta(_vm.Cmyk.ScaledMagenta + 1)); });
            EventUtil.HandleKey(Key.Up, CmykScaledYellow, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledYellow(_vm.Cmyk.ScaledYellow + 1)); });
            EventUtil.HandleKey(Key.Up, CmykScaledKey, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledKey(_vm.Cmyk.ScaledKey + 1)); });

            EventUtil.HandleKey(Key.Down, CmykCyan, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithCyan(_vm.Cmyk.Cyan - 0.01)); });
            EventUtil.HandleKey(Key.Down, CmykMagenta, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithMagenta(_vm.Cmyk.Magenta - 0.01)); });
            EventUtil.HandleKey(Key.Down, CmykYellow, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithYellow(_vm.Cmyk.Yellow - 0.01)); });
            EventUtil.HandleKey(Key.Down, CmykKey, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithKey(_vm.Cmyk.Key - 0.01)); });
            EventUtil.HandleKey(Key.Down, CmykScaledCyan, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledCyan(_vm.Cmyk.ScaledCyan - 1)); });
            EventUtil.HandleKey(Key.Down, CmykScaledMagenta, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledMagenta(_vm.Cmyk.ScaledMagenta - 1)); });
            EventUtil.HandleKey(Key.Down, CmykScaledYellow, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledYellow(_vm.Cmyk.ScaledYellow - 1)); });
            EventUtil.HandleKey(Key.Down, CmykScaledKey, () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledKey(_vm.Cmyk.ScaledKey - 1)); });

            EventUtil.HandleMouseWheel(CmykCyan,
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithCyan(_vm.Cmyk.Cyan + 0.01)); },
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithCyan(_vm.Cmyk.Cyan - 0.01)); });
            EventUtil.HandleMouseWheel(CmykMagenta,
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithMagenta(_vm.Cmyk.Magenta + 0.01)); },
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithMagenta(_vm.Cmyk.Magenta - 0.01)); });
            EventUtil.HandleMouseWheel(CmykYellow,
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithYellow(_vm.Cmyk.Yellow + 0.01)); },
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithYellow(_vm.Cmyk.Yellow - 0.01)); });
            EventUtil.HandleMouseWheel(CmykKey,
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithKey(_vm.Cmyk.Key + 0.01)); },
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithKey(_vm.Cmyk.Key - 0.01)); });
            EventUtil.HandleMouseWheel(CmykScaledCyan,
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledCyan(_vm.Cmyk.ScaledCyan + 1)); },
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledCyan(_vm.Cmyk.ScaledCyan - 1)); });
            EventUtil.HandleMouseWheel(CmykScaledMagenta,
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledMagenta(_vm.Cmyk.ScaledMagenta + 1)); },
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledMagenta(_vm.Cmyk.ScaledMagenta - 1)); });
            EventUtil.HandleMouseWheel(CmykScaledYellow,
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledYellow(_vm.Cmyk.ScaledYellow + 1)); },
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledYellow(_vm.Cmyk.ScaledYellow - 1)); });
            EventUtil.HandleMouseWheel(CmykScaledKey,
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledKey(_vm.Cmyk.ScaledKey + 1)); },
                                       () => { _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledKey(_vm.Cmyk.ScaledKey - 1)); });

            EventUtil.HandleSliderChange(CmykCyanSlider, value => { _vm.RefreshFromCmyk(_vm.Cmyk.WithCyan(value)); });
            EventUtil.HandleSliderChange(CmykMagentaSlider, value => { _vm.RefreshFromCmyk(_vm.Cmyk.WithMagenta(value)); });
            EventUtil.HandleSliderChange(CmykYellowSlider, value => { _vm.RefreshFromCmyk(_vm.Cmyk.WithYellow(value)); });
            EventUtil.HandleSliderChange(CmykKeySlider, value => { _vm.RefreshFromCmyk(_vm.Cmyk.WithKey(value)); });

            _vm.PropertyChangedByUser += (o, ev) =>
            {
                bool   isDouble;
                double doubleVal;
                Cmyk?  nullableCmyk;

                switch (ev.PropertyName)
                {
                case nameof(MainWindowViewModel.CmykText):
                    nullableCmyk = Cmyk.FromString(_vm.CmykText);
                    if (nullableCmyk.HasValue)
                    {
                        _vm.RefreshFromCmyk(nullableCmyk.Value);
                    }
                    break;

                case nameof(MainWindowViewModel.CmykScaledText):
                    nullableCmyk = Cmyk.FromScaledString(_vm.CmykScaledText);
                    if (nullableCmyk.HasValue)
                    {
                        _vm.RefreshFromCmyk(nullableCmyk.Value);
                    }
                    break;

                case nameof(MainWindowViewModel.CmykCyan):
                    isDouble = double.TryParse(_vm.CmykCyan, out doubleVal);
                    if (!isDouble || !Cmyk.IsValidComponent(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromCmyk(_vm.Cmyk.WithCyan(doubleVal));
                    break;

                case nameof(MainWindowViewModel.CmykMagenta):
                    isDouble = double.TryParse(_vm.CmykMagenta, out doubleVal);
                    if (!isDouble || !Cmyk.IsValidComponent(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromCmyk(_vm.Cmyk.WithMagenta(doubleVal));
                    break;

                case nameof(MainWindowViewModel.CmykYellow):
                    isDouble = double.TryParse(_vm.CmykYellow, out doubleVal);
                    if (!isDouble || !Cmyk.IsValidComponent(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromCmyk(_vm.Cmyk.WithYellow(doubleVal));
                    break;

                case nameof(MainWindowViewModel.CmykKey):
                    isDouble = double.TryParse(_vm.CmykKey, out doubleVal);
                    if (!isDouble || !Cmyk.IsValidComponent(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromCmyk(_vm.Cmyk.WithKey(doubleVal));
                    break;

                case nameof(MainWindowViewModel.CmykScaledCyan):
                    isDouble = double.TryParse(_vm.CmykScaledCyan, out doubleVal);
                    if (!isDouble || !Cmyk.IsValidScaledComponent(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledCyan(doubleVal));
                    break;

                case nameof(MainWindowViewModel.CmykScaledMagenta):
                    isDouble = double.TryParse(_vm.CmykScaledMagenta, out doubleVal);
                    if (!isDouble || !Cmyk.IsValidScaledComponent(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledMagenta(doubleVal));
                    break;

                case nameof(MainWindowViewModel.CmykScaledYellow):
                    isDouble = double.TryParse(_vm.CmykScaledYellow, out doubleVal);
                    if (!isDouble || !Cmyk.IsValidScaledComponent(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledYellow(doubleVal));
                    break;

                case nameof(MainWindowViewModel.CmykScaledKey):
                    isDouble = double.TryParse(_vm.CmykScaledKey, out doubleVal);
                    if (!isDouble || !Cmyk.IsValidScaledComponent(doubleVal))
                    {
                        return;
                    }
                    _vm.RefreshFromCmyk(_vm.Cmyk.WithScaledKey(doubleVal));
                    break;
                }
            };

            SetupClipboardButton(CmykScaledTextClipboardButton, () => _vm.CmykScaledText);
            SetupClipboardButton(CmykTextClipboardButton, () => _vm.CmykText);

            //
            // LAB fields
            //

            EventUtil.HandleKey(Key.Up, LabL, () => { _vm.RefreshFromLab(_vm.Lab.WithL(_vm.Lab.L + 1)); });
            EventUtil.HandleKey(Key.Up, LabA, () => { _vm.RefreshFromLab(_vm.Lab.WithA(_vm.Lab.A + 1)); });
            EventUtil.HandleKey(Key.Up, LabB, () => { _vm.RefreshFromLab(_vm.Lab.WithB(_vm.Lab.B + 1)); });

            EventUtil.HandleKey(Key.Down, LabL, () => { _vm.RefreshFromLab(_vm.Lab.WithL(_vm.Lab.L - 1)); });
            EventUtil.HandleKey(Key.Down, LabA, () => { _vm.RefreshFromLab(_vm.Lab.WithA(_vm.Lab.A - 1)); });
            EventUtil.HandleKey(Key.Down, LabB, () => { _vm.RefreshFromLab(_vm.Lab.WithB(_vm.Lab.B - 1)); });

            EventUtil.HandleMouseWheel(LabL,
                                       () => { _vm.RefreshFromLab(_vm.Lab.WithL(_vm.Lab.L + 1)); },
                                       () => { _vm.RefreshFromLab(_vm.Lab.WithL(_vm.Lab.L - 1)); });
            EventUtil.HandleMouseWheel(LabA,
                                       () => { _vm.RefreshFromLab(_vm.Lab.WithA(_vm.Lab.A + 1)); },
                                       () => { _vm.RefreshFromLab(_vm.Lab.WithA(_vm.Lab.A - 1)); });
            EventUtil.HandleMouseWheel(LabB,
                                       () => { _vm.RefreshFromLab(_vm.Lab.WithB(_vm.Lab.B + 1)); },
                                       () => { _vm.RefreshFromLab(_vm.Lab.WithB(_vm.Lab.B - 1)); });

            EventUtil.HandleSliderChange(LabLSlider, value => { _vm.RefreshFromLab(_vm.Lab.WithL(value)); });
            EventUtil.HandleSliderChange(LabASlider, value => { _vm.RefreshFromLab(_vm.Lab.WithA(value)); });
            EventUtil.HandleSliderChange(LabBSlider, value => { _vm.RefreshFromLab(_vm.Lab.WithB(value)); });

            _vm.PropertyChangedByUser += (o, ev) =>
            {
                switch (ev.PropertyName)
                {
                case nameof(MainWindowViewModel.LabText):
                    var nullableLab = Lab.FromString(_vm.LabText);
                    if (nullableLab.HasValue)
                    {
                        _vm.RefreshFromLab(nullableLab.Value);
                    }
                    break;

                case nameof(MainWindowViewModel.LabL):
                    if (!Lab.IsValidL(_vm.LabL))
                    {
                        return;
                    }
                    _vm.RefreshFromLab(_vm.Lab.WithL(_vm.LabL));
                    break;

                case nameof(MainWindowViewModel.LabA):
                    if (!Lab.IsValidA(_vm.LabA))
                    {
                        return;
                    }
                    _vm.RefreshFromLab(_vm.Lab.WithA(_vm.LabA));
                    break;

                case nameof(MainWindowViewModel.LabB):
                    if (!Lab.IsValidB(_vm.LabB))
                    {
                        return;
                    }
                    _vm.RefreshFromLab(_vm.Lab.WithB(_vm.LabB));
                    break;
                }
            };

            SetupClipboardButton(LabTextClipboardButton, () => _vm.LabText);

            //
            // ----
            //

            // Hack to bring window to front on start. (I had instances where I would open the tool
            // only for it to come up behind my current window.)
            if (!IsVisible)
            {
                Show();
            }
            WindowState = WindowState.Normal;
            Activate();
            Topmost = true;
            Topmost = false;
            Focus();
        }