From 5aacdbb22cb6caf34bb1a38dfd4345b7732520f3 Mon Sep 17 00:00:00 2001 From: Ray Berezowski <6616212+rberezowski@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:50:46 -0400 Subject: [PATCH] Add drag and drop item creation --- MainWindow.xaml.cs | 288 ++++++++++++++++++++++++++++++++++++++++- README.md | 2 +- TaskbarLauncher.csproj | 4 +- docs/INSTALL.md | 1 + 4 files changed, 287 insertions(+), 8 deletions(-) diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 8265bee..cd1cacf 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -3,6 +3,7 @@ using System.Windows.Controls; using System.Windows.Forms; using System.Windows.Media; using System.IO; +using System.Text; using System.Windows.Input; using TaskbarLauncher.Models; using TaskbarLauncher.Services; @@ -156,15 +157,42 @@ public partial class MainWindow : Window private void ItemsTree_DragOver(object sender, System.Windows.DragEventArgs e) { - e.Effects = CanDrop(e) ? System.Windows.DragDropEffects.Move : System.Windows.DragDropEffects.None; + if (CanMoveDroppedItem(e)) + { + e.Effects = System.Windows.DragDropEffects.Move; + } + else if (CanAddExternalDroppedItems(e.Data)) + { + e.Effects = System.Windows.DragDropEffects.Copy; + } + else + { + e.Effects = System.Windows.DragDropEffects.None; + } + e.Handled = true; } private void ItemsTree_Drop(object sender, System.Windows.DragEventArgs e) { - if (controller is null || - !e.Data.GetDataPresent(typeof(LauncherItem)) || - e.Data.GetData(typeof(LauncherItem)) is not LauncherItem draggedItem) + if (controller is null) + { + return; + } + + if (e.Data.GetDataPresent(typeof(LauncherItem)) && + e.Data.GetData(typeof(LauncherItem)) is LauncherItem draggedItem) + { + MoveDroppedItem(e, draggedItem); + return; + } + + AddExternalDroppedItems(e); + } + + private void MoveDroppedItem(System.Windows.DragEventArgs e, LauncherItem draggedItem) + { + if (controller is null) { return; } @@ -213,7 +241,59 @@ public partial class MainWindow : Window e.Handled = true; } - private bool CanDrop(System.Windows.DragEventArgs e) + private void AddExternalDroppedItems(System.Windows.DragEventArgs e) + { + if (controller is null) + { + return; + } + + List droppedItems = CreateItemsFromDropData(e.Data, controller.Config.DisplayMode); + if (droppedItems.Count == 0) + { + return; + } + + ApplyCurrentItem(); + + TreeViewItem? targetTreeItem = FindAncestor(e.OriginalSource as DependencyObject); + LauncherItem? targetItem = targetTreeItem?.Tag as LauncherItem; + if (targetItem is null) + { + controller.Config.Items.AddRange(droppedItems); + } + else if (targetItem.Type == LauncherItemType.Menu) + { + targetItem.Children.AddRange(droppedItems); + if (targetTreeItem is not null) + { + targetTreeItem.IsExpanded = true; + } + } + else + { + List? targetSiblings = FindSiblings(controller.Config.Items, targetItem); + if (targetSiblings is null) + { + controller.Config.Items.AddRange(droppedItems); + } + else + { + int targetIndex = targetSiblings.IndexOf(targetItem); + targetSiblings.InsertRange(targetIndex + 1, droppedItems); + } + } + + LauncherItem itemToSelect = droppedItems[0]; + selectedPath = FindItemPath(itemToSelect); + RefreshView(itemToSelect); + StatusText.Text = droppedItems.Count == 1 + ? $"Added {itemToSelect.Title}. Click Save to keep it." + : $"Added {droppedItems.Count} items. Click Save to keep them."; + e.Handled = true; + } + + private bool CanMoveDroppedItem(System.Windows.DragEventArgs e) { if (!e.Data.GetDataPresent(typeof(LauncherItem)) || e.Data.GetData(typeof(LauncherItem)) is not LauncherItem draggedItem) @@ -226,6 +306,204 @@ public partial class MainWindow : Window return targetItem is null || (!ReferenceEquals(draggedItem, targetItem) && !ContainsItem(draggedItem, targetItem)); } + private static bool CanAddExternalDroppedItems(System.Windows.IDataObject data) + { + return data.GetDataPresent(System.Windows.DataFormats.FileDrop) || + data.GetDataPresent(System.Windows.DataFormats.UnicodeText) || + data.GetDataPresent(System.Windows.DataFormats.Text) || + data.GetDataPresent("UniformResourceLocatorW") || + data.GetDataPresent("UniformResourceLocator"); + } + + private static List CreateItemsFromDropData(System.Windows.IDataObject data, LauncherDisplayMode displayMode) + { + List items = []; + + if (data.GetDataPresent(System.Windows.DataFormats.FileDrop) && + data.GetData(System.Windows.DataFormats.FileDrop) is string[] paths) + { + foreach (string path in paths) + { + LauncherItem? item = CreateItemFromPath(path, displayMode); + if (item is not null) + { + items.Add(item); + } + } + } + + if (items.Count > 0) + { + return items; + } + + string? droppedText = GetDroppedText(data); + if (!string.IsNullOrWhiteSpace(droppedText)) + { + foreach (string line in droppedText.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + LauncherItem? item = CreateItemFromText(line, displayMode); + if (item is not null) + { + items.Add(item); + } + } + } + + return items; + } + + private static string? GetDroppedText(System.Windows.IDataObject data) + { + if (data.GetDataPresent(System.Windows.DataFormats.UnicodeText)) + { + return data.GetData(System.Windows.DataFormats.UnicodeText) as string; + } + + if (data.GetDataPresent(System.Windows.DataFormats.Text)) + { + return data.GetData(System.Windows.DataFormats.Text) as string; + } + + return GetDroppedUrlFormatText(data, "UniformResourceLocatorW", Encoding.Unicode) ?? + GetDroppedUrlFormatText(data, "UniformResourceLocator", Encoding.UTF8); + } + + private static string? GetDroppedUrlFormatText(System.Windows.IDataObject data, string format, Encoding encoding) + { + if (!data.GetDataPresent(format)) + { + return null; + } + + object? rawData = data.GetData(format); + return rawData switch + { + string text => text, + byte[] bytes => DecodeDroppedUrlBytes(bytes, encoding), + MemoryStream stream => DecodeDroppedUrlBytes(stream.ToArray(), encoding), + _ => null + }; + } + + private static string DecodeDroppedUrlBytes(byte[] bytes, Encoding encoding) + { + return encoding.GetString(bytes).TrimEnd('\0', '\r', '\n', ' '); + } + + private static LauncherItem? CreateItemFromText(string text, LauncherDisplayMode displayMode) + { + string target = text.Trim(); + if (!Uri.TryCreate(target, UriKind.Absolute, out Uri? uri) || + (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) + { + return null; + } + + return new LauncherItem + { + Title = GetWebsiteTitle(uri), + Type = LauncherItemType.Website, + Target = uri.ToString(), + IconPath = uri.ToString(), + DisplayMode = displayMode + }; + } + + private static LauncherItem CreateWebsiteItem(Uri uri, string title, LauncherDisplayMode displayMode) + { + return new LauncherItem + { + Title = title, + Type = LauncherItemType.Website, + Target = uri.ToString(), + IconPath = uri.ToString(), + DisplayMode = displayMode + }; + } + + private static LauncherItem? CreateItemFromPath(string path, LauncherDisplayMode displayMode) + { + if (string.IsNullOrWhiteSpace(path)) + { + return null; + } + + string fullPath = path.Trim(); + if (Directory.Exists(fullPath)) + { + return new LauncherItem + { + Title = new DirectoryInfo(fullPath).Name, + Type = LauncherItemType.Folder, + Target = fullPath, + IconPath = fullPath, + DisplayMode = displayMode + }; + } + + if (!File.Exists(fullPath)) + { + return null; + } + + if (Path.GetExtension(fullPath).Equals(".url", StringComparison.OrdinalIgnoreCase)) + { + return CreateItemFromInternetShortcut(fullPath, displayMode); + } + + LauncherItemType type = IsApplicationPath(fullPath) ? LauncherItemType.App : LauncherItemType.File; + return new LauncherItem + { + Title = Path.GetFileNameWithoutExtension(fullPath), + Type = type, + Target = fullPath, + IconPath = fullPath, + DisplayMode = displayMode + }; + } + + private static LauncherItem? CreateItemFromInternetShortcut(string path, LauncherDisplayMode displayMode) + { + foreach (string line in File.ReadLines(path)) + { + if (!line.StartsWith("URL=", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + string target = line["URL=".Length..].Trim(); + if (Uri.TryCreate(target, UriKind.Absolute, out Uri? uri) && + (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)) + { + return CreateWebsiteItem(uri, Path.GetFileNameWithoutExtension(path), displayMode); + } + } + + return null; + } + + private static bool IsApplicationPath(string path) + { + string extension = Path.GetExtension(path); + return extension.Equals(".exe", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".lnk", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".bat", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".cmd", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".ps1", StringComparison.OrdinalIgnoreCase); + } + + private static string GetWebsiteTitle(Uri uri) + { + string host = uri.Host; + if (host.StartsWith("www.", StringComparison.OrdinalIgnoreCase)) + { + host = host[4..]; + } + + return string.IsNullOrWhiteSpace(host) ? uri.ToString() : host; + } + private static bool ContainsItem(LauncherItem parent, LauncherItem possibleChild) { foreach (LauncherItem child in parent.Children) diff --git a/README.md b/README.md index 973f828..bf166b6 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Download the compiled Windows release from: - System tray launcher - Global hotkey, default `Ctrl+Alt+Space` - Search/filter in the launcher popup -- Nested menus and drag/drop ordering in Settings +- Nested menus, drag/drop ordering, and drag/drop item creation in Settings - Per-item icon and display mode - Automatic icon selection from app/folder targets - Portable `config.json` diff --git a/TaskbarLauncher.csproj b/TaskbarLauncher.csproj index efa65f4..d1e9bdb 100644 --- a/TaskbarLauncher.csproj +++ b/TaskbarLauncher.csproj @@ -12,8 +12,8 @@ Ray Berezowski Assets\AppIcon.ico 1.0.1 - 1.0.1.1 - 1.0.1.1 + 1.0.1.2 + 1.0.1.2 1.0.1 diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 311e61b..fb5d7c9 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -30,6 +30,7 @@ From Settings you can: - Add submenus - Add folders, apps, files, and websites - Drag/drop items to reorder +- Drag/drop files, folders, shortcuts, or web links into the launcher item list - Set per-item icon sizes - Back up and restore `config.json` - Enable Start with Windows