Flow.Launcher/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs

494 lines
22 KiB
C#
Raw Permalink Normal View History

using System;
2020-05-25 08:58:04 +00:00
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
2025-04-09 04:14:07 +00:00
using System.Linq;
2020-05-25 08:58:04 +00:00
using System.Threading.Tasks;
using System.Windows;
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin.Explorer.Search;
2021-01-26 09:48:06 +00:00
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using Flow.Launcher.Plugin.Explorer.Helper;
2021-01-26 10:14:39 +00:00
using Flow.Launcher.Plugin.Explorer.ViewModels;
2020-05-25 08:58:04 +00:00
namespace Flow.Launcher.Plugin.Explorer
{
internal class ContextMenu : IContextMenu
{
2025-04-13 09:59:39 +00:00
private static readonly string ClassName = nameof(ContextMenu);
private PluginInitContext Context { get; set; }
private Settings Settings { get; set; }
public ContextMenu(PluginInitContext context, Settings settings)
{
Context = context;
Settings = settings;
}
2020-05-25 08:58:04 +00:00
public List<Result> LoadContextMenus(Result selectedResult)
{
var contextMenus = new List<Result>();
if (selectedResult.ContextData is SearchResult record)
{
if (record.Type == ResultType.File && !string.IsNullOrEmpty(Settings.EditorPath))
2022-12-19 20:07:07 +00:00
contextMenus.Add(CreateOpenWithEditorResult(record, Settings.EditorPath));
if ((record.Type == ResultType.Folder || record.Type == ResultType.Volume) && !string.IsNullOrEmpty(Settings.FolderEditorPath))
2022-12-19 20:07:07 +00:00
contextMenus.Add(CreateOpenWithEditorResult(record, Settings.FolderEditorPath));
if (record.Type == ResultType.Folder)
2022-08-17 00:52:53 +00:00
{
contextMenus.Add(CreateOpenWithShellResult(record));
if (record.WindowsIndexed)
{
contextMenus.Add(CreateAddToIndexSearchExclusionListResult(record));
}
2022-08-17 00:52:53 +00:00
}
contextMenus.Add(CreateOpenContainingFolderResult(record));
if (record.Type == ResultType.File)
{
contextMenus.Add(CreateOpenWithMenu(record));
}
if (record.WindowsIndexed)
{
contextMenus.Add(CreateOpenWindowsIndexingOptions());
}
2020-05-25 08:58:04 +00:00
var icoPath = (record.Type == ResultType.File) ? Constants.FileImagePath : Constants.FolderImagePath;
2022-12-21 06:01:09 +00:00
bool isFile = record.Type == ResultType.File;
if (Settings.QuickAccessLinks.All(x => !x.Path.Equals(record.FullPath, StringComparison.OrdinalIgnoreCase)))
{
contextMenus.Add(new Result
{
Title = Localize.plugin_explorer_add_to_quickaccess_title(),
SubTitle = Localize.plugin_explorer_add_to_quickaccess_subtitle(),
Action = (context) =>
{
Settings.QuickAccessLinks.Add(new AccessLink
{
2025-05-25 18:49:25 +00:00
Name = record.FullPath.GetPathName(),
Path = record.FullPath,
Type = record.Type
});
Context.API.ShowMsg(Localize.plugin_explorer_addfilefoldersuccess(),
Localize.plugin_explorer_addfilefoldersuccess_detail(),
Constants.ExplorerIconImageFullPath);
2021-01-26 10:14:39 +00:00
2021-01-26 19:17:24 +00:00
return true;
},
SubTitleToolTip = Localize.plugin_explorer_contextmenu_titletooltip(),
TitleToolTip = Localize.plugin_explorer_contextmenu_titletooltip(),
2022-12-12 04:08:22 +00:00
IcoPath = Constants.QuickAccessImagePath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue718"),
});
}
else
{
contextMenus.Add(new Result
{
Title = Localize.plugin_explorer_remove_from_quickaccess_title(),
SubTitle = Localize.plugin_explorer_remove_from_quickaccess_subtitle(),
Action = (context) =>
{
2022-12-20 12:57:05 +00:00
Settings.QuickAccessLinks.Remove(Settings.QuickAccessLinks.FirstOrDefault(x => string.Equals(x.Path, record.FullPath, StringComparison.OrdinalIgnoreCase)));
Context.API.ShowMsg(Localize.plugin_explorer_removefilefoldersuccess(),
Localize.plugin_explorer_removefilefoldersuccess_detail(),
Constants.ExplorerIconImageFullPath);
return true;
},
SubTitleToolTip = Localize.plugin_explorer_contextmenu_remove_titletooltip(),
TitleToolTip = Localize.plugin_explorer_contextmenu_remove_titletooltip(),
2022-12-12 04:08:22 +00:00
IcoPath = Constants.RemoveQuickAccessImagePath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uecc9")
});
}
2020-05-25 08:58:04 +00:00
contextMenus.Add(new Result
{
Title = Localize.plugin_explorer_copypath(),
SubTitle = Localize.plugin_explorer_copypath_subtitle(),
Action = _ =>
2020-05-25 08:58:04 +00:00
{
try
{
2023-06-09 09:59:49 +00:00
Context.API.CopyToClipboard(record.FullPath);
2020-05-25 08:58:04 +00:00
return true;
}
catch (Exception e)
{
2025-07-20 04:28:55 +00:00
LogException("Fail to set text in clipboard", e);
Context.API.ShowMsgError(Localize.plugin_explorer_fail_to_set_text());
2020-05-25 08:58:04 +00:00
return false;
}
},
2022-12-12 04:08:22 +00:00
IcoPath = Constants.CopyImagePath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue8c8")
2020-05-25 08:58:04 +00:00
});
2025-05-11 01:28:55 +00:00
contextMenus.Add(new Result
{
Title = Localize.plugin_explorer_copyname(),
SubTitle = Localize.plugin_explorer_copyname_subtitle(),
2025-05-11 01:28:55 +00:00
Action = _ =>
{
try
{
Context.API.CopyToClipboard(Path.GetFileName(record.FullPath));
return true;
}
catch (Exception e)
{
2025-07-20 04:28:55 +00:00
LogException("Fail to set text in clipboard", e);
Context.API.ShowMsgError(Localize.plugin_explorer_fail_to_set_text());
2025-05-11 01:28:55 +00:00
return false;
}
},
IcoPath = Constants.CopyImagePath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue8c8")
});
2020-05-25 08:58:04 +00:00
contextMenus.Add(new Result
{
Title = Localize.plugin_explorer_copyfilefolder(),
SubTitle = isFile ? Localize.plugin_explorer_copyfile_subtitle(): Localize.plugin_explorer_copyfolder_subtitle(),
Action = _ =>
2020-05-25 08:58:04 +00:00
{
try
{
2023-06-09 09:59:49 +00:00
Context.API.CopyToClipboard(record.FullPath, directCopy: true);
2020-05-25 08:58:04 +00:00
return true;
}
catch (Exception e)
{
2025-07-20 04:28:55 +00:00
LogException($"Fail to set file/folder in clipboard", e);
Context.API.ShowMsgError(Localize.plugin_explorer_fail_to_set_files());
2020-05-25 08:58:04 +00:00
return false;
}
},
2022-12-12 04:08:22 +00:00
IcoPath = icoPath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uf12b")
2020-05-25 08:58:04 +00:00
});
if (record.Type is ResultType.File or ResultType.Folder)
2020-05-25 08:58:04 +00:00
contextMenus.Add(new Result
{
Title = Localize.plugin_explorer_deletefilefolder(),
SubTitle = isFile ? Localize.plugin_explorer_deletefile_subtitle(): Localize.plugin_explorer_deletefolder_subtitle(),
2020-05-25 08:58:04 +00:00
Action = (context) =>
{
try
{
if (Context.API.ShowMsgBox(
Localize.plugin_explorer_delete_folder_link(record.FullPath),
Localize.plugin_explorer_deletefilefolder(),
MessageBoxButton.OKCancel,
MessageBoxImage.Warning)
== MessageBoxResult.Cancel)
2020-06-21 22:43:59 +00:00
return false;
2022-12-21 06:01:09 +00:00
if (isFile)
2020-05-25 08:58:04 +00:00
File.Delete(record.FullPath);
else
Directory.Delete(record.FullPath, true);
2020-06-21 22:43:59 +00:00
_ = Task.Run(() =>
2020-06-21 22:43:59 +00:00
{
Context.API.ShowMsg(Localize.plugin_explorer_deletefilefoldersuccess(),
Localize.plugin_explorer_deletefilefoldersuccess_detail(record.FullPath),
Constants.ExplorerIconImageFullPath);
2020-06-21 22:43:59 +00:00
});
2020-05-25 08:58:04 +00:00
}
catch (Exception e)
{
2025-07-20 04:07:15 +00:00
LogException($"Fail to delete {record.FullPath}", e);
Context.API.ShowMsgError(Localize.plugin_explorer_fail_to_delete(record.FullPath));
2020-05-25 08:58:04 +00:00
return false;
}
return true;
},
2022-12-12 04:08:22 +00:00
IcoPath = Constants.DeleteFileFolderImagePath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue74d")
2020-05-25 08:58:04 +00:00
});
if (record.Type is not ResultType.Volume)
{
contextMenus.Add(new Result()
{
Title = Localize.plugin_explorer_show_contextmenu_title(),
IcoPath = Constants.ShowContextMenuImagePath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue700"),
Action = _ =>
{
if (record.Type is ResultType.Volume)
return false;
ResultManager.ShowNativeContextMenu(record.FullPath, record.Type);
return false;
2022-11-29 10:50:53 +00:00
},
});
}
2020-05-25 08:58:04 +00:00
if (record.Type == ResultType.File && CanRunAsDifferentUser(record.FullPath))
contextMenus.Add(new Result
{
Title = Localize.plugin_explorer_runasdifferentuser(),
SubTitle = Localize.plugin_explorer_runasdifferentuser_subtitle(),
2020-05-25 08:58:04 +00:00
Action = (context) =>
{
try
{
_ = Task.Run(() => ShellCommand.RunAsDifferentUser(record.FullPath.SetProcessStartInfo()));
2020-05-25 08:58:04 +00:00
}
catch (FileNotFoundException e)
{
2025-07-20 04:07:15 +00:00
Context.API.ShowMsgError(
Localize.plugin_explorer_plugin_name(),
Localize.plugin_explorer_file_not_found(e.Message));
return false;
2020-05-25 08:58:04 +00:00
}
return true;
},
IcoPath = Constants.DifferentUserIconImagePath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue748"),
2020-05-25 08:58:04 +00:00
});
if (record.Type is ResultType.File or ResultType.Folder && Settings.ShowInlinedWindowsContextMenu)
{
var includedItems = Settings
.WindowsContextMenuIncludedItems
.Replace("\r", "")
.Split("\n")
.Where(v => !string.IsNullOrWhiteSpace(v))
.ToArray();
var excludedItems = Settings
.WindowsContextMenuExcludedItems
.Replace("\r", "")
.Split("\n")
.Where(v => !string.IsNullOrWhiteSpace(v))
.ToArray();
var menuItems = ShellContextMenuDisplayHelper
.GetContextMenuWithIcons(record.FullPath)
.Where(contextMenuItem =>
(includedItems.Length == 0 || includedItems.Any(filter =>
contextMenuItem.Label.Contains(filter, StringComparison.OrdinalIgnoreCase)
)) &&
(excludedItems.Length == 0 || !excludedItems.Any(filter =>
contextMenuItem.Label.Contains(filter, StringComparison.OrdinalIgnoreCase)
))
);
foreach (var menuItem in menuItems)
{
contextMenus.Add(new Result
{
Title = menuItem.Label,
Icon = () => menuItem.Icon,
Action = _ =>
{
ShellContextMenuDisplayHelper.ExecuteContextMenuItem(record.FullPath, menuItem.CommandId);
return true;
}
});
}
}
2020-05-25 08:58:04 +00:00
}
return contextMenus;
}
private Result CreateOpenContainingFolderResult(SearchResult record)
{
return new Result
{
Title = Localize.plugin_explorer_opencontainingfolder(),
SubTitle = Localize.plugin_explorer_opencontainingfolder_subtitle(),
2020-05-25 08:58:04 +00:00
Action = _ =>
{
try
{
Context.API.OpenDirectory(Path.GetDirectoryName(record.FullPath), record.FullPath);
2020-05-25 08:58:04 +00:00
}
catch (Exception e)
{
2025-07-20 04:07:15 +00:00
LogException($"Fail to open file at {record.FullPath}", e);
Context.API.ShowMsgError(Localize.plugin_explorer_fail_to_open(record.FullPath));
2020-05-25 08:58:04 +00:00
return false;
}
return true;
},
2022-12-12 04:08:22 +00:00
IcoPath = Constants.FolderImagePath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue838")
2020-05-25 08:58:04 +00:00
};
}
2022-12-19 20:07:07 +00:00
private Result CreateOpenWithEditorResult(SearchResult record, string editorPath)
2020-05-25 08:58:04 +00:00
{
var name = $"{Localize.plugin_explorer_openwitheditor()} {Path.GetFileNameWithoutExtension(editorPath)}";
2020-05-25 08:58:04 +00:00
return new Result
{
Title = name,
Action = _ =>
{
try
{
2022-12-13 17:18:57 +00:00
Process.Start(new ProcessStartInfo()
{
FileName = editorPath,
ArgumentList = { record.FullPath }
});
2020-05-25 08:58:04 +00:00
return true;
}
catch (Exception e)
{
var message = Localize.plugin_explorer_openwitheditor_error(record.FullPath, Path.GetFileNameWithoutExtension(editorPath), editorPath);
2020-05-25 08:58:04 +00:00
LogException(message, e);
Context.API.ShowMsgError(message);
2020-05-25 08:58:04 +00:00
return false;
}
},
2023-01-02 17:37:01 +00:00
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue70f"),
IcoPath = Constants.FileImagePath
2020-05-25 08:58:04 +00:00
};
}
2022-08-17 00:52:53 +00:00
private Result CreateOpenWithShellResult(SearchResult record)
{
string shellPath = Settings.ShellPath;
var name = $"{Localize.plugin_explorer_openwithshell()} {Path.GetFileNameWithoutExtension(shellPath)}";
2022-08-17 00:52:53 +00:00
return new Result
{
Title = name,
Action = _ =>
{
try
{
Process.Start(new ProcessStartInfo()
{
FileName = shellPath, WorkingDirectory = record.FullPath
2022-08-17 00:52:53 +00:00
});
return true;
}
catch (Exception e)
{
var message = Localize.plugin_explorer_openwithshell_error(record.FullPath, Path.GetFileNameWithoutExtension(shellPath), shellPath);
2022-08-17 00:52:53 +00:00
LogException(message, e);
Context.API.ShowMsgError(message);
return false;
}
},
2023-01-02 17:37:01 +00:00
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue756"),
IcoPath = Constants.FolderImagePath
2022-08-17 00:52:53 +00:00
};
}
2020-05-25 08:58:04 +00:00
private Result CreateAddToIndexSearchExclusionListResult(SearchResult record)
{
return new Result
{
Title = Localize.plugin_explorer_excludefromindexsearch(),
SubTitle = Localize.plugin_explorer_path()+ " " + record.FullPath,
2025-04-09 05:09:37 +00:00
Action = c_ =>
{
2022-12-20 12:57:05 +00:00
if (!Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => string.Equals(x.Path, record.FullPath, StringComparison.OrdinalIgnoreCase)))
Settings.IndexSearchExcludedSubdirectoryPaths.Add(new AccessLink
{
Path = record.FullPath
});
2025-04-09 05:09:37 +00:00
_ = Task.Run(() =>
{
Context.API.ShowMsg(Localize.plugin_explorer_excludedfromindexsearch_msg(),
Localize.plugin_explorer_path()+
" " + record.FullPath, Constants.ExplorerIconImageFullPath);
// so the new path can be persisted to storage and not wait till next ViewModel save.
Context.API.SaveAppAllSettings();
});
return false;
},
IcoPath = Constants.ExcludeFromIndexImagePath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\uf140"),
};
}
2020-06-08 10:31:48 +00:00
private Result CreateOpenWindowsIndexingOptions()
{
return new Result
{
Title = Localize.plugin_explorer_openindexingoptions(),
SubTitle = Localize.plugin_explorer_openindexingoptions_subtitle(),
Action = _ =>
{
try
{
var psi = new ProcessStartInfo
{
FileName = "control.exe",
UseShellExecute = true,
Arguments = "srchadmin.dll"
};
Process.Start(psi);
return true;
}
catch (Exception e)
{
var message = Localize.plugin_explorer_openindexingoptions_errormsg();
LogException(message, e);
Context.API.ShowMsgError(message);
return false;
}
},
IcoPath = Constants.IndexingOptionsIconImagePath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue773"),
};
}
private static Result CreateOpenWithMenu(SearchResult record)
{
return new Result
{
Title = Localize.plugin_explorer_openwith(),
SubTitle = Localize.plugin_explorer_openwith_subtitle(),
Action = _ =>
{
Process.Start("rundll32.exe", $"{Path.Combine(Environment.SystemDirectory, "shell32.dll")},OpenAs_RunDLL {record.FullPath}");
return true;
},
IcoPath = Constants.ShowContextMenuImagePath,
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue7ac"),
};
}
2023-01-02 17:37:01 +00:00
private void LogException(string message, Exception e)
2020-05-25 08:58:04 +00:00
{
2025-04-13 09:59:39 +00:00
Context.API.LogException(ClassName, message, e);
2020-05-25 08:58:04 +00:00
}
2025-04-09 04:14:07 +00:00
private static bool CanRunAsDifferentUser(string path)
2020-05-25 08:58:04 +00:00
{
2025-04-09 04:14:07 +00:00
return Path.GetExtension(path) switch
2020-05-25 08:58:04 +00:00
{
2025-04-09 04:14:07 +00:00
".exe" or ".bat" or ".msi" => true,
_ => false,
};
2020-05-25 08:58:04 +00:00
}
}
}