Browse Source

Improved drag & drop UX

CPKreuz 2 years ago
parent
commit
a93c150b5c

+ 39 - 0
src/PixiEditor/Helpers/Collections/ActionDisplayList.cs

@@ -0,0 +1,39 @@
+using System.Collections;
+
+namespace PixiEditor.Helpers.Collections;
+
+public class ActionDisplayList : IEnumerable<KeyValuePair<string, string>>
+{
+    private Dictionary<string, string> _dictionary = new();
+    private Action notifyUpdate;
+
+    public ActionDisplayList(Action notifyUpdate)
+    {
+        this.notifyUpdate = notifyUpdate;
+    }
+
+    public string this[string key]
+    {
+        get => _dictionary[key];
+        set
+        {
+            if (value == null)
+            {
+                _dictionary.Remove(key);
+                notifyUpdate();
+                return;
+            }
+            
+            _dictionary[key] = value;
+            notifyUpdate();
+        }
+    }
+
+    public string GetActive() => _dictionary.Last().Value;
+
+    public bool HasActive() => _dictionary.Count != 0;
+
+    public IEnumerator<KeyValuePair<string, string>> GetEnumerator() => _dictionary.GetEnumerator();
+
+    IEnumerator IEnumerable.GetEnumerator() => _dictionary.GetEnumerator();
+}

+ 21 - 12
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -68,13 +68,13 @@ internal static class ClipboardController
     /// <summary>
     /// <summary>
     ///     Pastes image from clipboard into new layer.
     ///     Pastes image from clipboard into new layer.
     /// </summary>
     /// </summary>
-    public static bool TryPasteFromClipboard(DocumentViewModel document)
+    public static bool TryPaste(DocumentViewModel document, DataObject data, bool pasteAsNew = false)
     {
     {
-        List<(string? name, Surface image)> images = GetImagesFromClipboard();
+        List<(string? name, Surface image)> images = GetImage(data);
         if (images.Count == 0)
         if (images.Count == 0)
             return false;
             return false;
 
 
-        if (images.Count == 1)
+        if (images.Count == 1 && !pasteAsNew)
         {
         {
             document.Operations.PasteImageWithTransform(images[0].image, VecI.Zero);
             document.Operations.PasteImageWithTransform(images[0].image, VecI.Zero);
             return true;
             return true;
@@ -84,12 +84,19 @@ internal static class ClipboardController
         return true;
         return true;
     }
     }
 
 
+    /// <summary>
+    ///     Pastes image from clipboard into new layer.
+    /// </summary>
+    public static bool TryPasteFromClipboard(DocumentViewModel document) =>
+        TryPaste(document, ClipboardHelper.TryGetDataObject());
+
+    public static List<(string? name, Surface image)> GetImagesFromClipboard() => GetImage(ClipboardHelper.TryGetDataObject());
+
     /// <summary>
     /// <summary>
     /// Gets images from clipboard, supported PNG, Dib and Bitmap.
     /// Gets images from clipboard, supported PNG, Dib and Bitmap.
     /// </summary>
     /// </summary>
-    public static List<(string? name, Surface image)> GetImagesFromClipboard()
+    public static List<(string? name, Surface image)> GetImage(DataObject? data)
     {
     {
-        DataObject data = ClipboardHelper.TryGetDataObject();
         List<(string? name, Surface image)> surfaces = new();
         List<(string? name, Surface image)> surfaces = new();
 
 
         if (data == null)
         if (data == null)
@@ -121,15 +128,16 @@ internal static class ClipboardController
         return surfaces;
         return surfaces;
     }
     }
 
 
-    public static bool IsImageInClipboard()
+    public static bool IsImageInClipboard() => IsImage(ClipboardHelper.TryGetDataObject());
+    
+    public static bool IsImage(DataObject? dataObject)
     {
     {
-        DataObject dao = ClipboardHelper.TryGetDataObject();
-        if (dao == null)
+        if (dataObject == null)
             return false;
             return false;
 
 
         try
         try
         {
         {
-            var files = dao.GetFileDropList();
+            var files = dataObject.GetFileDropList();
             if (files != null)
             if (files != null)
             {
             {
                 foreach (var file in files)
                 foreach (var file in files)
@@ -146,8 +154,7 @@ internal static class ClipboardController
             return false;
             return false;
         }
         }
 
 
-        return dao.GetDataPresent("PNG") || dao.GetDataPresent(DataFormats.Dib) ||
-               dao.GetDataPresent(DataFormats.Bitmap) || dao.GetDataPresent(DataFormats.FileDrop);
+        return HasData(dataObject, "PNG", DataFormats.Dib, DataFormats.Bitmap);
     }
     }
 
 
     private static BitmapSource FromPNG(DataObject data)
     private static BitmapSource FromPNG(DataObject data)
@@ -158,6 +165,8 @@ internal static class ClipboardController
         return decoder.Frames[0];
         return decoder.Frames[0];
     }
     }
 
 
+    private static bool HasData(DataObject dataObject, params string[] formats) => formats.Any(dataObject.GetDataPresent);
+    
     private static bool TryExtractSingleImage(DataObject data, [NotNullWhen(true)] out Surface? result)
     private static bool TryExtractSingleImage(DataObject data, [NotNullWhen(true)] out Surface? result)
     {
     {
         try
         try
@@ -168,7 +177,7 @@ internal static class ClipboardController
             {
             {
                 source = FromPNG(data);
                 source = FromPNG(data);
             }
             }
-            else if (data.GetDataPresent(DataFormats.Dib) || data.GetDataPresent(DataFormats.Bitmap))
+            else if (HasData(data, DataFormats.Dib, DataFormats.Bitmap))
             {
             {
                 source = Clipboard.GetImage();
                 source = Clipboard.GetImage();
             }
             }

+ 0 - 1
src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -299,7 +299,6 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         DocumentViewModel doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         DocumentViewModel doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
         if (doc is null)
             return;
             return;
-        ViewModelMain.Current.ActionDisplay = "";
 
 
         ExportFileDialog info = new ExportFileDialog(doc.SizeBindable);
         ExportFileDialog info = new ExportFileDialog(doc.SizeBindable);
         if (info.ShowDialog())
         if (info.ShowDialog())

+ 18 - 4
src/PixiEditor/ViewModels/SubViewModels/Main/LayersViewModel.cs

@@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
 using System.Windows;
 using System.Windows;
 using System.Windows.Input;
 using System.Windows.Input;
 using System.Windows.Media.Imaging;
 using System.Windows.Media.Imaging;
+using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using Microsoft.Win32;
 using Microsoft.Win32;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Enums;
@@ -330,8 +331,20 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         Owner.DocumentManagerSubViewModel.ActiveDocument is not null && Owner.DocumentManagerSubViewModel.ActiveDocument.ReferenceLayerViewModel.ReferenceBitmap is null;
         Owner.DocumentManagerSubViewModel.ActiveDocument is not null && Owner.DocumentManagerSubViewModel.ActiveDocument.ReferenceLayerViewModel.ReferenceBitmap is null;
 
 
     [Evaluator.CanExecute("PixiEditor.Layer.ReferenceLayerDoesntExistAndHasClipboardContent")]
     [Evaluator.CanExecute("PixiEditor.Layer.ReferenceLayerDoesntExistAndHasClipboardContent")]
-    public bool ReferenceLayerDoesntExistAndHasClipboardContent() =>
-        ReferenceLayerDoesntExist() && Owner.ClipboardSubViewModel.CanPaste();
+    public bool ReferenceLayerDoesntExistAndHasClipboardContent(DataObject data)
+    {
+        if (!ReferenceLayerDoesntExist())
+        {
+            return false;
+        }
+        
+        if (data != null)
+        {
+            return Owner.DocumentIsNotNull(null) && ClipboardController.IsImage(data);
+        }
+        
+        return Owner.ClipboardSubViewModel.CanPaste();
+    }
 
 
     [Command.Basic("PixiEditor.Layer.ImportReferenceLayer", "Add reference layer", "Add reference layer", CanExecute = "PixiEditor.Layer.ReferenceLayerDoesntExist")]
     [Command.Basic("PixiEditor.Layer.ImportReferenceLayer", "Add reference layer", "Add reference layer", CanExecute = "PixiEditor.Layer.ReferenceLayerDoesntExist")]
     public void ImportReferenceLayer()
     public void ImportReferenceLayer()
@@ -366,13 +379,14 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     }
     }
 
 
     [Command.Basic("PixiEditor.Layer.PasteReferenceLayer", "Paste reference layer", "Paste reference layer from clipboard", IconPath = "Commands/PixiEditor/Clipboard/Paste.png", CanExecute = "PixiEditor.Layer.ReferenceLayerDoesntExistAndHasClipboardContent")]
     [Command.Basic("PixiEditor.Layer.PasteReferenceLayer", "Paste reference layer", "Paste reference layer from clipboard", IconPath = "Commands/PixiEditor/Clipboard/Paste.png", CanExecute = "PixiEditor.Layer.ReferenceLayerDoesntExistAndHasClipboardContent")]
-    public unsafe void PasteReferenceLayer()
+    public unsafe void PasteReferenceLayer(DataObject data)
     {
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
         if (doc is null)
             return;
             return;
 
 
-        var surface = ClipboardController.GetImagesFromClipboard().First();
+        var surface = (data == null ? ClipboardController.GetImagesFromClipboard() : ClipboardController.GetImage(data)).First();
+        
         var bitmap = surface.image.ToWriteableBitmap();
         var bitmap = surface.image.ToWriteableBitmap();
         
         
         byte[] pixels = new byte[bitmap.PixelWidth * bitmap.PixelHeight * 4];
         byte[] pixels = new byte[bitmap.PixelWidth * bitmap.PixelHeight * 4];

+ 5 - 32
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -2,6 +2,7 @@
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
+using PixiEditor.Helpers.Collections;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
@@ -16,9 +17,6 @@ namespace PixiEditor.ViewModels;
 
 
 internal class ViewModelMain : ViewModelBase
 internal class ViewModelMain : ViewModelBase
 {
 {
-    private string actionDisplay;
-    private bool overrideActionDisplay;
-
     public static ViewModelMain Current { get; set; }
     public static ViewModelMain Current { get; set; }
 
 
     public IServiceProvider Services { get; private set; }
     public IServiceProvider Services { get; private set; }
@@ -73,39 +71,14 @@ internal class ViewModelMain : ViewModelBase
 
 
     public IPreferences Preferences { get; set; }
     public IPreferences Preferences { get; set; }
 
 
-    public string ActionDisplay
-    {
-        get
-        {
-            if (OverrideActionDisplay)
-            {
-                return actionDisplay;
-            }
-
-            return ToolsSubViewModel.ActiveTool?.ActionDisplay;
-        }
-        set
-        {
-            actionDisplay = value;
-        }
-    }
+    public string ActiveActionDisplay => ActionDisplays.HasActive() ? ActionDisplays.GetActive() : ToolsSubViewModel.ActiveTool?.ActionDisplay;
 
 
-    /// <summary>
-    /// Gets or sets a value indicating whether a custom action display should be used. If false the action display of the selected tool will be used.
-    /// </summary>
-    public bool OverrideActionDisplay
-    {
-        get => overrideActionDisplay;
-        set
-        {
-            SetProperty(ref overrideActionDisplay, value);
-            RaisePropertyChanged(nameof(ActionDisplay));
-        }
-    }
+    public ActionDisplayList ActionDisplays { get; }
 
 
     public ViewModelMain(IServiceProvider serviceProvider)
     public ViewModelMain(IServiceProvider serviceProvider)
     {
     {
         Current = this;
         Current = this;
+        ActionDisplays = new ActionDisplayList(() => RaisePropertyChanged(nameof(ActiveActionDisplay)));
     }
     }
 
 
     public void Setup(IServiceProvider services)
     public void Setup(IServiceProvider services)
@@ -195,7 +168,7 @@ internal class ViewModelMain : ViewModelBase
 
 
     private void NotifyToolActionDisplayChanged()
     private void NotifyToolActionDisplayChanged()
     {
     {
-        if (!OverrideActionDisplay) RaisePropertyChanged(nameof(ActionDisplay));
+        if (!ActionDisplays.Any()) RaisePropertyChanged(nameof(ActiveActionDisplay));
     }
     }
 
 
     /// <summary>
     /// <summary>

+ 2 - 2
src/PixiEditor/Views/MainWindow.xaml

@@ -496,7 +496,7 @@
             </StackPanel>
             </StackPanel>
 
 
             <Grid Grid.Column="1" Grid.Row="2" Background="#303030" >
             <Grid Grid.Column="1" Grid.Row="2" Background="#303030" >
-                <Grid AllowDrop="True" Drop="MainWindow_Drop">
+                <Grid AllowDrop="True" Drop="MainWindow_Drop" DragEnter="MainWindow_DragEnter" DragLeave="MainWindow_DragLeave">
                     <DockingManager 
                     <DockingManager 
                         ActiveContent="{Binding WindowSubViewModel.ActiveWindow, Mode=TwoWay}"
                         ActiveContent="{Binding WindowSubViewModel.ActiveWindow, Mode=TwoWay}"
                         DocumentsSource="{Binding WindowSubViewModel.Viewports}">
                         DocumentsSource="{Binding WindowSubViewModel.Viewports}">
@@ -785,7 +785,7 @@
                 </Grid.ColumnDefinitions>
                 </Grid.ColumnDefinitions>
                 <DockPanel>
                 <DockPanel>
                     <TextBlock
                     <TextBlock
-                        Text="{Binding ActionDisplay}"
+                        Text="{Binding ActiveActionDisplay}"
                         Foreground="White"
                         Foreground="White"
                         FontSize="15"
                         FontSize="15"
                         Margin="10,0,0,0"
                         Margin="10,0,0,0"

+ 31 - 9
src/PixiEditor/Views/MainWindow.xaml.cs

@@ -6,6 +6,7 @@ using System.Windows.Media.Imaging;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Skia;
 using PixiEditor.DrawingApi.Skia;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Document;
@@ -184,16 +185,37 @@ internal partial class MainWindow : Window
 
 
     private void MainWindow_Drop(object sender, DragEventArgs e)
     private void MainWindow_Drop(object sender, DragEventArgs e)
     {
     {
-        if (e.Data.GetDataPresent(DataFormats.FileDrop))
+        if (!e.Data.GetDataPresent(DataFormats.FileDrop))
         {
         {
-            string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
-            if (files != null && files.Length > 0)
-            {
-                if (Importer.IsSupportedFile(files[0]))
-                {
-                    DataContext.FileSubViewModel.OpenFromPath(files[0]);
-                }
-            }
+            return;
+        }
+
+        string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
+        
+        if (files is { Length: > 0 } && Importer.IsSupportedFile(files[0]))
+        {
+            DataContext.FileSubViewModel.OpenFromPath(files[0]);
+        }
+    }
+
+    private void MainWindow_DragEnter(object sender, DragEventArgs e)
+    {
+        if (!ClipboardController.IsImage((DataObject)e.Data))
+        {
+            e.Effects = DragDropEffects.None;
+            return;
         }
         }
+
+        DataContext.ActionDisplays[nameof(MainWindow_Drop)] = "Import as new file";
+    }
+
+    private void MainWindow_DragLeave(object sender, DragEventArgs e)
+    {
+        if (!e.Data.GetDataPresent(DataFormats.FileDrop))
+        {
+            return;
+        }
+
+        DataContext.ActionDisplays[nameof(MainWindow_Drop)] = null;
     }
     }
 }
 }

+ 42 - 3
src/PixiEditor/Views/UserControls/Layers/LayersManager.xaml.cs

@@ -3,6 +3,7 @@ using System.Windows.Controls;
 using System.Windows.Input;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media;
 using System.Windows.Threading;
 using System.Windows.Threading;
+using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Document;
@@ -114,20 +115,58 @@ internal partial class LayersManager : UserControl
 
 
     private void Grid_Drop(object sender, DragEventArgs e)
     private void Grid_Drop(object sender, DragEventArgs e)
     {
     {
+        if (ActiveDocument == null)
+        {
+            return;
+        }
+
         dropBorder.BorderBrush = Brushes.Transparent;
         dropBorder.BorderBrush = Brushes.Transparent;
         Guid? droppedGuid = LayerControl.ExtractMemberGuid(e.Data);
         Guid? droppedGuid = LayerControl.ExtractMemberGuid(e.Data);
-        if (droppedGuid is null || ActiveDocument is null)
-            return;
-        ActiveDocument.Operations.MoveStructureMember((Guid)droppedGuid, ActiveDocument.StructureRoot.Children[0].GuidValue, StructureMemberPlacement.Below);
+
+        if (droppedGuid is not null && ActiveDocument is not null)
+        {
+            ActiveDocument.Operations.MoveStructureMember((Guid)droppedGuid,
+                ActiveDocument.StructureRoot.Children[0].GuidValue, StructureMemberPlacement.Below);
+            e.Handled = true;
+        }
+
+        if (ClipboardController.TryPaste(ActiveDocument, (DataObject)e.Data, true))
+        {
+            e.Handled = true;
+        }
     }
     }
 
 
     private void Grid_DragEnter(object sender, DragEventArgs e)
     private void Grid_DragEnter(object sender, DragEventArgs e)
     {
     {
+        if (ActiveDocument == null)
+        {
+            return;
+        }
+        
+        var member = LayerControl.ExtractMemberGuid(e.Data);
+
+        if (member == null)
+        {
+            if (!ClipboardController.IsImage((DataObject)e.Data))
+            {
+                return;
+            }
+
+            ViewModelMain.Current.ActionDisplays[nameof(LayersManager)] = "Import as new layer";
+            e.Effects = DragDropEffects.Copy;
+        }
+        else
+        {
+            e.Effects = DragDropEffects.Move;
+        }
+        
         ((Border)sender).BorderBrush = highlightColor;
         ((Border)sender).BorderBrush = highlightColor;
+        e.Handled = true;
     }
     }
 
 
     private void Grid_DragLeave(object sender, DragEventArgs e)
     private void Grid_DragLeave(object sender, DragEventArgs e)
     {
     {
+        ViewModelMain.Current.ActionDisplays[nameof(LayersManager)] = null;
         ((Border)sender).BorderBrush = Brushes.Transparent;
         ((Border)sender).BorderBrush = Brushes.Transparent;
     }
     }
 
 

+ 2 - 1
src/PixiEditor/Views/UserControls/Layers/ReferenceLayer.xaml

@@ -12,7 +12,8 @@
              xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
              xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
              mc:Ignorable="d" 
              mc:Ignorable="d" 
              d:DesignHeight="60" d:DesignWidth="350" VerticalAlignment="Center" Name="uc">
              d:DesignHeight="60" d:DesignWidth="350" VerticalAlignment="Center" Name="uc">
-    <Border BorderBrush="{StaticResource DarkerAccentColor}" BorderThickness="0 2 0 0" MinWidth="60" Focusable="True">
+    <Border BorderBrush="{StaticResource DarkerAccentColor}" BorderThickness="0 2 0 0" MinWidth="60"
+            Focusable="True" AllowDrop="True" DragEnter="ReferenceLayer_DragEnter" DragLeave="ReferenceLayer_DragLeave" Drop="ReferenceLayer_Drop">
         <i:Interaction.Behaviors>
         <i:Interaction.Behaviors>
             <behaviors:ClearFocusOnClickBehavior/>
             <behaviors:ClearFocusOnClickBehavior/>
         </i:Interaction.Behaviors>
         </i:Interaction.Behaviors>

+ 27 - 0
src/PixiEditor/Views/UserControls/Layers/ReferenceLayer.xaml.cs

@@ -2,6 +2,8 @@
 using System.Windows.Controls;
 using System.Windows.Controls;
 using System.Windows.Input;
 using System.Windows.Input;
 using Microsoft.Win32;
 using Microsoft.Win32;
+using PixiEditor.Models.Commands;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using PixiEditor.ViewModels.SubViewModels.Document;
 using PixiEditor.ViewModels.SubViewModels.Document;
 
 
@@ -23,4 +25,29 @@ internal partial class ReferenceLayer : UserControl
     {
     {
         InitializeComponent();
         InitializeComponent();
     }
     }
+
+    private void ReferenceLayer_DragEnter(object sender, DragEventArgs e)
+    {
+        ViewModelMain.Current.ActionDisplays[nameof(ReferenceLayer_Drop)] = "Import as reference layer";
+        e.Handled = true;
+    }
+
+    private void ReferenceLayer_DragLeave(object sender, DragEventArgs e)
+    {
+        ViewModelMain.Current.ActionDisplays[nameof(ReferenceLayer_Drop)] = null;
+        e.Handled = true;
+    }
+
+    private void ReferenceLayer_Drop(object sender, DragEventArgs e)
+    {
+        var command = CommandController.Current.Commands["PixiEditor.Layer.PasteReferenceLayer"];
+
+        if (!command.Methods.CanExecute(e.Data))
+        {
+            return;
+        }
+
+        command.Methods.Execute(e.Data);
+        e.Handled = true;
+    }
 }
 }

+ 2 - 3
src/PixiEditor/Views/UserControls/PreviewWindow.xaml.cs

@@ -65,7 +65,7 @@ internal partial class PreviewWindow : UserControl
     {
     {
         if (ViewModelMain.Current != null)
         if (ViewModelMain.Current != null)
         {
         {
-            ViewModelMain.Current.OverrideActionDisplay = false;
+            ViewModelMain.Current.ActionDisplays[nameof(PreviewWindow)] = null;
         }
         }
     }
     }
 
 
@@ -73,8 +73,7 @@ internal partial class PreviewWindow : UserControl
     {
     {
         if (ViewModelMain.Current != null)
         if (ViewModelMain.Current != null)
         {
         {
-            ViewModelMain.Current.ActionDisplay = "Right-click to pick color, Shift-right-click to copy color to clipboard";
-            ViewModelMain.Current.OverrideActionDisplay = true;
+            ViewModelMain.Current.ActionDisplays[nameof(PreviewWindow)] = "Right-click to pick color, Shift-right-click to copy color to clipboard";
         }
         }
     }
     }