Merge branch 'dev' of github.com:Flow-Launcher/Flow.Launcher into ProgressBarDispatcher

This commit is contained in:
Hongtao Zhang 2022-11-23 00:23:39 -06:00
commit 71938cdbf8
No known key found for this signature in database
GPG key ID: 75F655B91C7AC9BB
55 changed files with 3129 additions and 28731 deletions

View file

@ -1,4 +1,4 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
@ -21,6 +21,7 @@ namespace Flow.Launcher.Infrastructure
public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins); public static readonly string PreinstalledDirectory = Path.Combine(ProgramDirectory, Plugins);
public const string Issue = "https://github.com/Flow-Launcher/Flow.Launcher/issues/new"; public const string Issue = "https://github.com/Flow-Launcher/Flow.Launcher/issues/new";
public static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location.NonNull()).ProductVersion; public static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location.NonNull()).ProductVersion;
public static readonly string Dev = "Dev";
public const string Documentation = "https://flowlauncher.com/docs/#/usage-tips"; public const string Documentation = "https://flowlauncher.com/docs/#/usage-tips";
public static readonly int ThumbnailSize = 64; public static readonly int ThumbnailSize = 64;

View file

@ -58,22 +58,11 @@
<PackageReference Include="NLog.Schema" Version="4.7.10" /> <PackageReference Include="NLog.Schema" Version="4.7.10" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.13.0" /> <PackageReference Include="NLog.Web.AspNetCore" Version="4.13.0" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" /> <PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
<PackageReference Include="System.Drawing.Common" Version="5.0.2" /> <PackageReference Include="System.Drawing.Common" Version="5.0.3" />
<!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled--> <!--ToolGood.Words.Pinyin v3.0.2.6 results in high memory usage when search with pinyin is enabled-->
<!--Bumping to it or higher needs to test and ensure this is no longer a problem--> <!--Bumping to it or higher needs to test and ensure this is no longer a problem-->
<PackageReference Include="ToolGood.Words.Pinyin" Version="3.0.1.4" /> <PackageReference Include="ToolGood.Words.Pinyin" Version="3.0.1.4" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="pinyindb\pinyin_gwoyeu_mapping.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="pinyindb\pinyin_mapping.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="pinyindb\unicode_to_hanyu_pinyin.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project> </Project>

View file

@ -1,4 +1,4 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using NLog; using NLog;

View file

@ -15,7 +15,7 @@ namespace Flow.Launcher.Infrastructure
private List<int> originalIndexs = new List<int>(); private List<int> originalIndexs = new List<int>();
private List<int> translatedIndexs = new List<int>(); private List<int> translatedIndexs = new List<int>();
private int translaedLength = 0; private int translatedLength = 0;
public string key { get; private set; } public string key { get; private set; }
@ -32,13 +32,13 @@ namespace Flow.Launcher.Infrastructure
originalIndexs.Add(originalIndex); originalIndexs.Add(originalIndex);
translatedIndexs.Add(translatedIndex); translatedIndexs.Add(translatedIndex);
translatedIndexs.Add(translatedIndex + length); translatedIndexs.Add(translatedIndex + length);
translaedLength += length - 1; translatedLength += length - 1;
} }
public int MapToOriginalIndex(int translatedIndex) public int MapToOriginalIndex(int translatedIndex)
{ {
if (translatedIndex > translatedIndexs.Last()) if (translatedIndex > translatedIndexs.Last())
return translatedIndex - translaedLength - 1; return translatedIndex - translatedLength - 1;
int lowerBound = 0; int lowerBound = 0;
int upperBound = originalIndexs.Count - 1; int upperBound = originalIndexs.Count - 1;
@ -83,7 +83,7 @@ namespace Flow.Launcher.Infrastructure
translatedIndex < translatedIndexs[upperBound * 2]) translatedIndex < translatedIndexs[upperBound * 2])
{ {
int indexDef = 0; int indexDef = 0;
for (int j = 0; j < upperBound; j++) for (int j = 0; j < upperBound; j++)
{ {
indexDef += translatedIndexs[j * 2 + 1] - translatedIndexs[j * 2]; indexDef += translatedIndexs[j * 2 + 1] - translatedIndexs[j * 2];
@ -102,9 +102,24 @@ namespace Flow.Launcher.Infrastructure
} }
} }
/// <summary>
/// Translate a language to English letters using a given rule.
/// </summary>
public interface IAlphabet public interface IAlphabet
{ {
/// <summary>
/// Translate a string to English letters, using a given rule.
/// </summary>
/// <param name="stringToTranslate">String to translate.</param>
/// <returns></returns>
public (string translation, TranslationMapping map) Translate(string stringToTranslate); public (string translation, TranslationMapping map) Translate(string stringToTranslate);
/// <summary>
/// Determine if a string can be translated to English letter with this Alphabet.
/// </summary>
/// <param name="stringToTranslate">String to translate.</param>
/// <returns></returns>
public bool CanBeTranslated(string stringToTranslate);
} }
public class PinyinAlphabet : IAlphabet public class PinyinAlphabet : IAlphabet
@ -119,63 +134,70 @@ namespace Flow.Launcher.Infrastructure
_settings = settings ?? throw new ArgumentNullException(nameof(settings)); _settings = settings ?? throw new ArgumentNullException(nameof(settings));
} }
public bool CanBeTranslated(string stringToTranslate)
{
return WordsHelper.HasChinese(stringToTranslate);
}
public (string translation, TranslationMapping map) Translate(string content) public (string translation, TranslationMapping map) Translate(string content)
{ {
if (_settings.ShouldUsePinyin) if (_settings.ShouldUsePinyin)
{ {
if (!_pinyinCache.ContainsKey(content)) if (!_pinyinCache.ContainsKey(content))
{ {
if (WordsHelper.HasChinese(content)) return BuildCacheFromContent(content);
{
var resultList = WordsHelper.GetPinyinList(content);
StringBuilder resultBuilder = new StringBuilder();
TranslationMapping map = new TranslationMapping();
bool pre = false;
for (int i = 0; i < resultList.Length; i++)
{
if (content[i] >= 0x3400 && content[i] <= 0x9FD5)
{
map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1);
resultBuilder.Append(' ');
resultBuilder.Append(resultList[i]);
pre = true;
}
else
{
if (pre)
{
pre = false;
resultBuilder.Append(' ');
}
resultBuilder.Append(resultList[i]);
}
}
map.endConstruct();
var key = resultBuilder.ToString();
map.setKey(key);
return _pinyinCache[content] = (key, map);
}
else
{
return (content, null);
}
} }
else else
{ {
return _pinyinCache[content]; return _pinyinCache[content];
} }
} }
return (content, null);
}
private (string translation, TranslationMapping map) BuildCacheFromContent(string content)
{
if (WordsHelper.HasChinese(content))
{
var resultList = WordsHelper.GetPinyinList(content);
StringBuilder resultBuilder = new StringBuilder();
TranslationMapping map = new TranslationMapping();
bool pre = false;
for (int i = 0; i < resultList.Length; i++)
{
if (content[i] >= 0x3400 && content[i] <= 0x9FD5)
{
map.AddNewIndex(i, resultBuilder.Length, resultList[i].Length + 1);
resultBuilder.Append(' ');
resultBuilder.Append(resultList[i]);
pre = true;
}
else
{
if (pre)
{
pre = false;
resultBuilder.Append(' ');
}
resultBuilder.Append(resultList[i]);
}
}
map.endConstruct();
var key = resultBuilder.ToString();
map.setKey(key);
return _pinyinCache[content] = (key, map);
}
else else
{ {
return (content, null); return (content, null);
} }
} }
} }
} }

View file

@ -1,4 +1,4 @@
using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.Plugin.SharedModels;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -60,8 +60,13 @@ namespace Flow.Launcher.Infrastructure
return new MatchResult(false, UserSettingSearchPrecision); return new MatchResult(false, UserSettingSearchPrecision);
query = query.Trim(); query = query.Trim();
TranslationMapping translationMapping; TranslationMapping translationMapping = null;
(stringToCompare, translationMapping) = _alphabet?.Translate(stringToCompare) ?? (stringToCompare, null); if (_alphabet is not null && !_alphabet.CanBeTranslated(query))
{
// We assume that if a query can be translated (containing characters of a language, like Chinese)
// it actually means user doesn't want it to be translated to English letters.
(stringToCompare, translationMapping) = _alphabet.Translate(stringToCompare);
}
var currentAcronymQueryIndex = 0; var currentAcronymQueryIndex = 0;
var acronymMatchData = new List<int>(); var acronymMatchData = new List<int>();

View file

@ -0,0 +1,65 @@
using System;
using System.Text.Json.Serialization;
namespace Flow.Launcher.Infrastructure.UserSettings
{
public abstract class ShortcutBaseModel
{
public string Key { get; set; }
[JsonIgnore]
public Func<string> Expand { get; set; } = () => { return ""; };
public override bool Equals(object obj)
{
return obj is ShortcutBaseModel other &&
Key == other.Key;
}
public override int GetHashCode()
{
return Key.GetHashCode();
}
}
public class CustomShortcutModel : ShortcutBaseModel
{
public string Value { get; set; }
[JsonConstructorAttribute]
public CustomShortcutModel(string key, string value)
{
Key = key;
Value = value;
Expand = () => { return Value; };
}
public void Deconstruct(out string key, out string value)
{
key = Key;
value = Value;
}
public static implicit operator (string Key, string Value)(CustomShortcutModel shortcut)
{
return (shortcut.Key, shortcut.Value);
}
public static implicit operator CustomShortcutModel((string Key, string Value) shortcut)
{
return new CustomShortcutModel(shortcut.Key, shortcut.Value);
}
}
public class BuiltinShortcutModel : ShortcutBaseModel
{
public string Description { get; set; }
public BuiltinShortcutModel(string key, string description, Func<string> expand)
{
Key = key;
Description = description;
Expand = expand ?? (() => { return ""; });
}
}
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Drawing; using System.Drawing;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Windows;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher; using Flow.Launcher;
@ -51,6 +52,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public double SettingWindowHeight { get; set; } = 700; public double SettingWindowHeight { get; set; } = 700;
public double SettingWindowTop { get; set; } public double SettingWindowTop { get; set; }
public double SettingWindowLeft { get; set; } public double SettingWindowLeft { get; set; }
public System.Windows.WindowState SettingWindowState { get; set; } = WindowState.Normal;
public int CustomExplorerIndex { get; set; } = 0; public int CustomExplorerIndex { get; set; } = 0;
@ -129,8 +131,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
PrivateArg = "-private", PrivateArg = "-private",
EnablePrivate = false, EnablePrivate = false,
Editable = false Editable = false
} },
,
new() new()
{ {
Name = "MS Edge", Name = "MS Edge",
@ -186,6 +187,13 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public ObservableCollection<CustomPluginHotkey> CustomPluginHotkeys { get; set; } = new ObservableCollection<CustomPluginHotkey>(); public ObservableCollection<CustomPluginHotkey> CustomPluginHotkeys { get; set; } = new ObservableCollection<CustomPluginHotkey>();
public ObservableCollection<CustomShortcutModel> CustomShortcuts { get; set; } = new ObservableCollection<CustomShortcutModel>();
[JsonIgnore]
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts { get; set; } = new ObservableCollection<BuiltinShortcutModel>() {
new BuiltinShortcutModel("{clipboard}", "shortcut_clipboard_description", Clipboard.GetText)
};
public bool DontPromptUpdateMsg { get; set; } public bool DontPromptUpdateMsg { get; set; }
public bool EnableUpdateLog { get; set; } public bool EnableUpdateLog { get; set; }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -37,7 +37,11 @@ namespace Flow.Launcher.Plugin
/// user's clipboard when Ctrl + C is pressed on a result. If the text is a file/directory path /// user's clipboard when Ctrl + C is pressed on a result. If the text is a file/directory path
/// flow will copy the actual file/folder instead of just the path text. /// flow will copy the actual file/folder instead of just the path text.
/// </summary> /// </summary>
public string CopyText { get; set; } = string.Empty; public string CopyText
{
get => string.IsNullOrEmpty(_copyText) ? SubTitle : _copyText;
set => _copyText = value;
}
/// <summary> /// <summary>
/// This holds the text which can be provided by plugin to help Flow autocomplete text /// This holds the text which can be provided by plugin to help Flow autocomplete text
@ -81,6 +85,7 @@ namespace Flow.Launcher.Plugin
/// Delegate to Get Image Source /// Delegate to Get Image Source
/// </summary> /// </summary>
public IconDelegate Icon; public IconDelegate Icon;
private string _copyText = string.Empty;
/// <summary> /// <summary>
/// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons) /// Information for Glyph Icon (Prioritized than IcoPath/Icon if user enable Glyph Icons)

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;

View file

@ -19,7 +19,7 @@ namespace Flow.Launcher.Test.Plugins
var app = new UWP.Application(); var app = new UWP.Application();
// Act // Act
var result = app.FormattedPriReferenceValue(packageName, rawPriReferenceValue); var result = UWP.Application.FormattedPriReferenceValue(packageName, rawPriReferenceValue);
// Assert // Assert
Assert.IsTrue(result == expectedFormat, Assert.IsTrue(result == expectedFormat,

View file

@ -0,0 +1,20 @@
using System;
using System.Globalization;
using System.Windows.Data;
using Flow.Launcher.Core.Resource;
namespace Flow.Launcher.Converters
{
public class TranlationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var key = value.ToString();
if (String.IsNullOrEmpty(key))
return key;
return InternationalizationManager.Instance.GetTranslation(key);
}
public object ConvertBack(object value, System.Type targetType, object parameter, CultureInfo culture) => throw new System.InvalidOperationException();
}
}

View file

@ -0,0 +1,160 @@
<Window
x:Class="Flow.Launcher.CustomShortcutSetting"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:flowlauncher="clr-namespace:Flow.Launcher"
Title="{DynamicResource customeQueryShortcutTitle}"
Width="530"
Background="{DynamicResource PopuBGColor}"
Foreground="{DynamicResource PopupTextColor}"
Icon="Images\app.png"
ResizeMode="NoResize"
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
<Window.InputBindings>
<KeyBinding Key="Escape" Command="Close" />
</Window.InputBindings>
<Window.CommandBindings>
<CommandBinding Command="Close" Executed="cmdEsc_OnPress" />
</Window.CommandBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="80" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Grid.Column="4"
Click="BtnCancel_OnClick"
Style="{StaticResource TitleBarCloseButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18,11 27,20 M 18,20 27,11"
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1">
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
</Grid>
</StackPanel>
<StackPanel Margin="26,0,26,0">
<StackPanel Grid.Row="0" Margin="0,0,0,12">
<TextBlock
Grid.Column="0"
Margin="0,0,0,0"
FontFamily="Segoe UI"
FontSize="20"
FontWeight="SemiBold"
Text="{DynamicResource customQueryShortcut}"
TextAlignment="Left" />
</StackPanel>
<StackPanel>
<TextBlock
FontSize="14"
Text="{DynamicResource customeQueryShortcutTips}"
TextAlignment="Left"
TextWrapping="WrapWithOverflow" />
</StackPanel>
<StackPanel Margin="0,20,0,0" Orientation="Horizontal">
<Grid Width="470">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource customShortcut}" />
<TextBox
Grid.Row="0"
Grid.Column="1"
Margin="10"
Text="{Binding Key}"
/>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource customShortcutExpansion}" />
<DockPanel
Grid.Row="1"
Grid.Column="1"
LastChildFill="True">
<Button
x:Name="btnTestShortcut"
Margin="0,0,10,0"
Padding="10,5,10,5"
Click="BtnTestShortcut_OnClick"
Content="{DynamicResource preview}"
DockPanel.Dock="Right" />
<TextBox
x:Name="tbExpand"
Margin="10"
HorizontalAlignment="Stretch"
Text="{Binding Value}"
VerticalAlignment="Center" />
</DockPanel>
</Grid>
</StackPanel>
</StackPanel>
</StackPanel>
<Border
Grid.Row="1"
Margin="0,14,0,0"
Background="{DynamicResource PopupButtonAreaBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0,1,0,0">
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button
x:Name="btnCancel"
MinWidth="140"
Margin="10,0,5,0"
Click="BtnCancel_OnClick"
Content="{DynamicResource cancel}" />
<Button
x:Name="btnAdd"
MinWidth="140"
Margin="5,0,10,0"
Click="BtnAdd_OnClick"
Style="{StaticResource AccentButtonStyle}">
<TextBlock x:Name="lblAdd" Text="{DynamicResource done}" />
</Button>
</StackPanel>
</Border>
</Grid>
</Window>

View file

@ -0,0 +1,73 @@
using Flow.Launcher.Core.Resource;
using Flow.Launcher.ViewModel;
using System;
using System.Windows;
using System.Windows.Input;
namespace Flow.Launcher
{
public partial class CustomShortcutSetting : Window
{
private SettingWindowViewModel viewModel;
public string Key { get; set; } = String.Empty;
public string Value { get; set; } = String.Empty;
private string originalKey { get; init; } = null;
private string originalValue { get; init; } = null;
private bool update { get; init; } = false;
public CustomShortcutSetting(SettingWindowViewModel vm)
{
viewModel = vm;
InitializeComponent();
}
public CustomShortcutSetting(string key, string value, SettingWindowViewModel vm)
{
viewModel = vm;
Key = key;
Value = value;
originalKey = key;
originalValue = value;
update = true;
InitializeComponent();
}
private void BtnCancel_OnClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void BtnAdd_OnClick(object sender, RoutedEventArgs e)
{
if (String.IsNullOrEmpty(Key) || String.IsNullOrEmpty(Value))
{
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("emptyShortcut"));
return;
}
// Check if key is modified or adding a new one
if (((update && originalKey != Key) || !update)
&& viewModel.ShortcutExists(Key))
{
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("duplicateShortcut"));
return;
}
DialogResult = !update || originalKey != Key || originalValue != Value;
Close();
}
private void cmdEsc_OnPress(object sender, ExecutedRoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void BtnTestShortcut_OnClick(object sender, RoutedEventArgs e)
{
App.API.ChangeQuery(tbExpand.Text);
Application.Current.MainWindow.Show();
Application.Current.MainWindow.Opacity = 1;
Application.Current.MainWindow.Focus();
}
}
}

View file

@ -89,7 +89,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="InputSimulator" Version="1.0.4" /> <PackageReference Include="InputSimulator" Version="1.0.4" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" /> <PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="ModernWpfUI" Version="0.9.4" /> <PackageReference Include="ModernWpfUI" Version="0.9.4" />
<PackageReference Include="NHotkey.Wpf" Version="2.1.0" /> <PackageReference Include="NHotkey.Wpf" Version="2.1.0" />
<PackageReference Include="NuGet.CommandLine" Version="5.7.2"> <PackageReference Include="NuGet.CommandLine" Version="5.7.2">
@ -98,6 +98,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" /> <PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
<PackageReference Include="SharpVectors" Version="1.7.6" /> <PackageReference Include="SharpVectors" Version="1.7.6" />
<PackageReference Include="VirtualizingWrapPanel" Version="1.5.7" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -115,12 +116,12 @@
<Target Name="PreBuild" BeforeTargets="PreBuildEvent"> <Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="taskkill /f /fi &quot;IMAGENAME eq Flow.Launcher.exe&quot;" /> <Exec Command="taskkill /f /fi &quot;IMAGENAME eq Flow.Launcher.exe&quot;" />
</Target> </Target>
<Target Name="RemoveDuplicateAnalyzers" BeforeTargets="CoreCompile"> <Target Name="RemoveDuplicateAnalyzers" BeforeTargets="CoreCompile">
<!-- Work around https://github.com/dotnet/wpf/issues/6792 --> <!-- Work around https://github.com/dotnet/wpf/issues/6792 -->
<ItemGroup> <ItemGroup>
<FilteredAnalyzer Include="@(Analyzer->Distinct())" /> <FilteredAnalyzer Include="@(Analyzer-&gt;Distinct())" />
<Analyzer Remove="@(Analyzer)" /> <Analyzer Remove="@(Analyzer)" />
<Analyzer Include="@(FilteredAnalyzer)" /> <Analyzer Include="@(FilteredAnalyzer)" />
</ItemGroup> </ItemGroup>

View file

@ -38,9 +38,8 @@
FontSize="13" FontSize="13"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource Color05B}" Foreground="{DynamicResource Color05B}"
Visibility="Visible"> Text="{DynamicResource flowlauncherPressHotkey}"
Press key Visibility="Visible" />
</TextBlock>
</Border> </Border>
</Popup> </Popup>
@ -49,8 +48,8 @@
Margin="0,0,18,0" Margin="0,0,18,0"
VerticalContentAlignment="Center" VerticalContentAlignment="Center"
input:InputMethod.IsInputMethodEnabled="False" input:InputMethod.IsInputMethodEnabled="False"
LostFocus="tbHotkey_LostFocus"
PreviewKeyDown="TbHotkey_OnPreviewKeyDown" PreviewKeyDown="TbHotkey_OnPreviewKeyDown"
TabIndex="100" TabIndex="100" />
LostFocus="tbHotkey_LostFocus"/>
</Grid> </Grid>
</UserControl> </UserControl>

View file

@ -9,6 +9,7 @@ using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using System.Threading; using System.Threading;
using System.Windows.Interop;
namespace Flow.Launcher namespace Flow.Launcher
{ {
@ -113,7 +114,7 @@ namespace Flow.Launcher
private void tbHotkey_LostFocus(object sender, RoutedEventArgs e) private void tbHotkey_LostFocus(object sender, RoutedEventArgs e)
{ {
tbMsg.Text = tbMsgTextOriginal; tbMsg.Text = tbMsgTextOriginal;
tbMsg.Foreground = tbMsgForegroundColorOriginal; tbMsg.SetResourceReference(TextBox.ForegroundProperty, "Color05B");
} }
} }
} }

View file

@ -27,7 +27,7 @@
<system:String x:Key="PositionResetToolTip">Reset search window position</system:String> <system:String x:Key="PositionResetToolTip">Reset search window position</system:String>
<!-- Setting General --> <!-- Setting General -->
<system:String x:Key="flowlauncher_settings">Flow Launcher Settings</system:String> <system:String x:Key="flowlauncher_settings">Settings</system:String>
<system:String x:Key="general">General</system:String> <system:String x:Key="general">General</system:String>
<system:String x:Key="portableMode">Portable Mode</system:String> <system:String x:Key="portableMode">Portable Mode</system:String>
<system:String x:Key="portableModeToolTIp">Store all settings and user data in one folder (Useful when used with removable drives or cloud services).</system:String> <system:String x:Key="portableModeToolTIp">Store all settings and user data in one folder (Useful when used with removable drives or cloud services).</system:String>
@ -92,6 +92,7 @@
<system:String x:Key="plugin_query_time">Query time:</system:String> <system:String x:Key="plugin_query_time">Query time:</system:String>
<system:String x:Key="plugin_query_version">| Version</system:String> <system:String x:Key="plugin_query_version">| Version</system:String>
<system:String x:Key="plugin_query_web">Website</system:String> <system:String x:Key="plugin_query_web">Website</system:String>
<system:String x:Key="plugin_uninstall">Uninstall</system:String>
<!-- Setting Plugin Store --> <!-- Setting Plugin Store -->
@ -104,9 +105,9 @@
<system:String x:Key="installbtn">Install</system:String> <system:String x:Key="installbtn">Install</system:String>
<system:String x:Key="uninstallbtn">Uninstall</system:String> <system:String x:Key="uninstallbtn">Uninstall</system:String>
<system:String x:Key="updatebtn">Update</system:String> <system:String x:Key="updatebtn">Update</system:String>
<system:String x:Key="LabelInstalledToolTip">Plug-in already installed</system:String> <system:String x:Key="LabelInstalledToolTip">Plugin already installed</system:String>
<system:String x:Key="LabelNew">New Version</system:String> <system:String x:Key="LabelNew">New Version</system:String>
<system:String x:Key="LabelNewToolTip">This plug-in has been updated within the last 7 days</system:String> <system:String x:Key="LabelNewToolTip">This plugin has been updated within the last 7 days</system:String>
<system:String x:Key="LabelUpdateToolTip">New Update is Available</system:String> <system:String x:Key="LabelUpdateToolTip">New Update is Available</system:String>
@ -144,18 +145,26 @@
<system:String x:Key="showOpenResultHotkey">Show Hotkey</system:String> <system:String x:Key="showOpenResultHotkey">Show Hotkey</system:String>
<system:String x:Key="showOpenResultHotkeyToolTip">Show result selection hotkey with results.</system:String> <system:String x:Key="showOpenResultHotkeyToolTip">Show result selection hotkey with results.</system:String>
<system:String x:Key="customQueryHotkey">Custom Query Hotkey</system:String> <system:String x:Key="customQueryHotkey">Custom Query Hotkey</system:String>
<system:String x:Key="customQueryShortcut">Custom Query Shortcut</system:String>
<system:String x:Key="builtinShortcuts">Built-in Shortcuts</system:String>
<system:String x:Key="customQuery">Query</system:String> <system:String x:Key="customQuery">Query</system:String>
<system:String x:Key="customShortcut">Shortcut</system:String>
<system:String x:Key="customShortcutExpansion">Expanded</system:String>
<system:String x:Key="builtinShortcutDescription">Description</system:String>
<system:String x:Key="delete">Delete</system:String> <system:String x:Key="delete">Delete</system:String>
<system:String x:Key="edit">Edit</system:String> <system:String x:Key="edit">Edit</system:String>
<system:String x:Key="add">Add</system:String> <system:String x:Key="add">Add</system:String>
<system:String x:Key="pleaseSelectAnItem">Please select an item</system:String> <system:String x:Key="pleaseSelectAnItem">Please select an item</system:String>
<system:String x:Key="deleteCustomHotkeyWarning">Are you sure you want to delete {0} plugin hotkey?</system:String> <system:String x:Key="deleteCustomHotkeyWarning">Are you sure you want to delete {0} plugin hotkey?</system:String>
<system:String x:Key="deleteCustomShortcutWarning">Are you sure you want to delete shortcut: {0} with expansion {1}?</system:String>
<system:String x:Key="shortcut_clipboard_description">Get text from clipboard.</system:String>
<system:String x:Key="queryWindowShadowEffect">Query window shadow effect</system:String> <system:String x:Key="queryWindowShadowEffect">Query window shadow effect</system:String>
<system:String x:Key="shadowEffectCPUUsage">Shadow effect has a substantial usage of GPU. Not recommended if your computer performance is limited.</system:String> <system:String x:Key="shadowEffectCPUUsage">Shadow effect has a substantial usage of GPU. Not recommended if your computer performance is limited.</system:String>
<system:String x:Key="windowWidthSize">Window Width Size</system:String> <system:String x:Key="windowWidthSize">Window Width Size</system:String>
<system:String x:Key="windowWidthSizeToolTip">You can also quickly adjust this by using Ctrl+[ and Ctrl+].</system:String> <system:String x:Key="windowWidthSizeToolTip">You can also quickly adjust this by using Ctrl+[ and Ctrl+].</system:String>
<system:String x:Key="useGlyphUI">Use Segoe Fluent Icons</system:String> <system:String x:Key="useGlyphUI">Use Segoe Fluent Icons</system:String>
<system:String x:Key="useGlyphUIEffect">Use Segoe Fluent Icons for query results where supported</system:String> <system:String x:Key="useGlyphUIEffect">Use Segoe Fluent Icons for query results where supported</system:String>
<system:String x:Key="flowlauncherPressHotkey">Press Key</system:String>
<!-- Setting Proxy --> <!-- Setting Proxy -->
<system:String x:Key="proxy">HTTP Proxy</system:String> <system:String x:Key="proxy">HTTP Proxy</system:String>
@ -179,6 +188,7 @@
<system:String x:Key="github">Github</system:String> <system:String x:Key="github">Github</system:String>
<system:String x:Key="docs">Docs</system:String> <system:String x:Key="docs">Docs</system:String>
<system:String x:Key="version">Version</system:String> <system:String x:Key="version">Version</system:String>
<system:String x:Key="icons">Icons</system:String>
<system:String x:Key="about_activate_times">You have activated Flow Launcher {0} times</system:String> <system:String x:Key="about_activate_times">You have activated Flow Launcher {0} times</system:String>
<system:String x:Key="checkUpdates">Check for Updates</system:String> <system:String x:Key="checkUpdates">Check for Updates</system:String>
<system:String x:Key="newVersionTips">New version {0} is available, would you like to restart Flow Launcher to use the update?</system:String> <system:String x:Key="newVersionTips">New version {0} is available, would you like to restart Flow Launcher to use the update?</system:String>
@ -241,6 +251,12 @@
<system:String x:Key="invalidPluginHotkey">Invalid plugin hotkey</system:String> <system:String x:Key="invalidPluginHotkey">Invalid plugin hotkey</system:String>
<system:String x:Key="update">Update</system:String> <system:String x:Key="update">Update</system:String>
<!-- Custom Query Shortcut Dialog -->
<system:String x:Key="customeQueryShortcutTitle">Custom Query Shortcut</system:String>
<system:String x:Key="customeQueryShortcutTips">Enter a shortcut that automatically expands to the specified query.</system:String>
<system:String x:Key="duplicateShortcut">Shortcut already exists, please enter a new Shortcut or edit the existing one.</system:String>
<system:String x:Key="emptyShortcut">Shortcut and/or its expansion is empty.</system:String>
<!-- Hotkey Control --> <!-- Hotkey Control -->
<system:String x:Key="hotkeyUnavailable">Hotkey Unavailable</system:String> <system:String x:Key="hotkeyUnavailable">Hotkey Unavailable</system:String>

View file

@ -6,6 +6,7 @@
xmlns:flowlauncher="clr-namespace:Flow.Launcher" xmlns:flowlauncher="clr-namespace:Flow.Launcher"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/" xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:vm="clr-namespace:Flow.Launcher.ViewModel" xmlns:vm="clr-namespace:Flow.Launcher.ViewModel"
Name="FlowMainWindow" Name="FlowMainWindow"
Title="Flow Launcher" Title="Flow Launcher"
@ -80,30 +81,26 @@
Command="{Binding LoadContextMenuCommand}" /> Command="{Binding LoadContextMenuCommand}" />
<KeyBinding Key="Left" <KeyBinding Key="Left"
Command="{Binding EscCommand}" /> Command="{Binding EscCommand}" />
<KeyBinding <KeyBinding Key="H"
Key="H"
Command="{Binding LoadHistoryCommand}" Command="{Binding LoadHistoryCommand}"
Modifiers="Ctrl" /> Modifiers="Ctrl" />
<KeyBinding Key="Right" Command="{Binding LoadContextMenuCommand}" /> <KeyBinding Key="Right"
<KeyBinding Key="Left" Command="{Binding EscCommand}" /> Command="{Binding LoadContextMenuCommand}" />
<KeyBinding <KeyBinding Key="Left"
Key="OemCloseBrackets" Command="{Binding EscCommand}" />
<KeyBinding Key="OemCloseBrackets"
Command="{Binding IncreaseWidthCommand}" Command="{Binding IncreaseWidthCommand}"
Modifiers="Control" /> Modifiers="Control" />
<KeyBinding <KeyBinding Key="OemOpenBrackets"
Key="OemOpenBrackets"
Command="{Binding DecreaseWidthCommand}" Command="{Binding DecreaseWidthCommand}"
Modifiers="Control" /> Modifiers="Control" />
<KeyBinding <KeyBinding Key="OemPlus"
Key="OemPlus"
Command="{Binding IncreaseMaxResultCommand}" Command="{Binding IncreaseMaxResultCommand}"
Modifiers="Control" /> Modifiers="Control" />
<KeyBinding <KeyBinding Key="OemMinus"
Key="OemMinus"
Command="{Binding DecreaseMaxResultCommand}" Command="{Binding DecreaseMaxResultCommand}"
Modifiers="Control" /> Modifiers="Control" />
<KeyBinding <KeyBinding Key="H"
Key="H"
Command="{Binding LoadHistoryCommand}" Command="{Binding LoadHistoryCommand}"
Modifiers="Ctrl" /> Modifiers="Ctrl" />
<KeyBinding Key="Enter" <KeyBinding Key="Enter"
@ -195,32 +192,51 @@
<TextBox.ContextMenu> <TextBox.ContextMenu>
<ContextMenu> <ContextMenu>
<MenuItem Command="ApplicationCommands.Cut" <MenuItem Command="ApplicationCommands.Cut"
Header="{DynamicResource cut}" /> Header="{DynamicResource cut}">
<MenuItem.Icon>
<ui:FontIcon Glyph="&#xe8c6;" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Command="ApplicationCommands.Copy" <MenuItem Command="ApplicationCommands.Copy"
Header="{DynamicResource copy}" /> Header="{DynamicResource copy}">
<MenuItem.Icon>
<ui:FontIcon Glyph="&#xe8c8;" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Command="ApplicationCommands.Paste" <MenuItem Command="ApplicationCommands.Paste"
Header="{DynamicResource paste}" /> Header="{DynamicResource paste}">
<MenuItem.Icon>
<ui:FontIcon Glyph="&#xe77f;" />
</MenuItem.Icon>
</MenuItem>
<Separator Margin="0" <Separator Margin="0"
Padding="0,4,0,4" Padding="0,4,0,4"
Background="{DynamicResource ContextSeparator}" /> Background="{DynamicResource ContextSeparator}" />
<MenuItem Click="OnContextMenusForSettingsClick" <MenuItem Click="OnContextMenusForSettingsClick"
Header="{DynamicResource flowlauncher_settings}" /> Header="{DynamicResource flowlauncher_settings}">
<MenuItem.Icon>
<ui:FontIcon Glyph="&#xe713;" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Command="{Binding EscCommand}" <MenuItem Command="{Binding EscCommand}"
Header="{DynamicResource closeWindow}" /> Header="{DynamicResource closeWindow}">
<MenuItem.Icon>
<ui:FontIcon Glyph="&#xe711;" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu> </ContextMenu>
</TextBox.ContextMenu> </TextBox.ContextMenu>
</TextBox> </TextBox>
<StackPanel x:Name="ClockPanel" <StackPanel x:Name="ClockPanel"
Style="{DynamicResource ClockPanel}" IsHitTestVisible="False"
IsHitTestVisible="False"> Style="{DynamicResource ClockPanel}">
<TextBlock <TextBlock Style="{DynamicResource DateBox}"
Visibility="{Binding Settings.UseDate, Converter={StaticResource BooleanToVisibilityConverter}}" Text="{Binding DateText}"
Style="{DynamicResource DateBox}" Visibility="{Binding Settings.UseDate, Converter={StaticResource BooleanToVisibilityConverter}}" />
Text="{Binding DateText}" />
<TextBlock Style="{DynamicResource ClockBox}" <TextBlock Style="{DynamicResource ClockBox}"
Visibility="{Binding Settings.UseClock, Converter={StaticResource BooleanToVisibilityConverter}}" Text="{Binding ClockText}"
Text="{Binding ClockText}"/> Visibility="{Binding Settings.UseClock, Converter={StaticResource BooleanToVisibilityConverter}}" />
</StackPanel> </StackPanel>
<Canvas Style="{DynamicResource SearchIconPosition}"> <Canvas Style="{DynamicResource SearchIconPosition}">
@ -299,7 +315,8 @@
<ContentControl> <ContentControl>
<flowlauncher:ResultListBox x:Name="ResultListBox" <flowlauncher:ResultListBox x:Name="ResultListBox"
DataContext="{Binding Results}" DataContext="{Binding Results}"
PreviewMouseDown="OnPreviewMouseButtonDown" /> LeftClickResultCommand="{Binding LeftClickResultCommand}"
RightClickResultCommand="{Binding RightClickResultCommand}" />
</ContentControl> </ContentControl>
</Border> </Border>
<Border Style="{DynamicResource WindowRadius}"> <Border Style="{DynamicResource WindowRadius}">
@ -316,7 +333,8 @@
<ContentControl> <ContentControl>
<flowlauncher:ResultListBox x:Name="ContextMenu" <flowlauncher:ResultListBox x:Name="ContextMenu"
DataContext="{Binding ContextMenu}" DataContext="{Binding ContextMenu}"
PreviewMouseDown="OnPreviewMouseButtonDown" /> LeftClickResultCommand="{Binding LeftClickResultCommand}"
RightClickResultCommand="{Binding RightClickResultCommand}" />
</ContentControl> </ContentControl>
</Border> </Border>
<Border Style="{DynamicResource WindowRadius}"> <Border Style="{DynamicResource WindowRadius}">
@ -333,7 +351,8 @@
<ContentControl> <ContentControl>
<flowlauncher:ResultListBox x:Name="History" <flowlauncher:ResultListBox x:Name="History"
DataContext="{Binding History}" DataContext="{Binding History}"
PreviewMouseDown="OnPreviewMouseButtonDown" /> LeftClickResultCommand="{Binding LeftClickResultCommand}"
RightClickResultCommand="{Binding RightClickResultCommand}" />
</ContentControl> </ContentControl>
</Border> </Border>
</StackPanel> </StackPanel>

View file

@ -20,8 +20,16 @@ using Flow.Launcher.Infrastructure;
using System.Windows.Media; using System.Windows.Media;
using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Hotkey;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using System.Text;
using DataObject = System.Windows.DataObject;
using System.Diagnostics;
using Microsoft.AspNetCore.Http;
using System.IO;
using System.Windows.Threading; using System.Windows.Threading;
using System.Windows.Data; using System.Windows.Data;
using ModernWpf.Controls;
using System.Drawing;
using System.Windows.Forms.Design.Behavior;
namespace Flow.Launcher namespace Flow.Launcher
{ {
@ -186,11 +194,12 @@ namespace Flow.Launcher
private void UpdateNotifyIconText() private void UpdateNotifyIconText()
{ {
var menu = contextMenu; var menu = contextMenu;
((MenuItem)menu.Items[1]).Header = InternationalizationManager.Instance.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")"; ((MenuItem)menu.Items[0]).Header = InternationalizationManager.Instance.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")";
((MenuItem)menu.Items[2]).Header = InternationalizationManager.Instance.GetTranslation("GameMode"); ((MenuItem)menu.Items[1]).Header = InternationalizationManager.Instance.GetTranslation("GameMode");
((MenuItem)menu.Items[3]).Header = InternationalizationManager.Instance.GetTranslation("PositionReset"); ((MenuItem)menu.Items[2]).Header = InternationalizationManager.Instance.GetTranslation("PositionReset");
((MenuItem)menu.Items[4]).Header = InternationalizationManager.Instance.GetTranslation("iconTraySettings"); ((MenuItem)menu.Items[3]).Header = InternationalizationManager.Instance.GetTranslation("iconTraySettings");
((MenuItem)menu.Items[5]).Header = InternationalizationManager.Instance.GetTranslation("iconTrayExit"); ((MenuItem)menu.Items[4]).Header = InternationalizationManager.Instance.GetTranslation("iconTrayExit");
} }
private void InitializeNotifyIcon() private void InitializeNotifyIcon()
@ -202,31 +211,35 @@ namespace Flow.Launcher
Visible = !_settings.HideNotifyIcon Visible = !_settings.HideNotifyIcon
}; };
contextMenu = new ContextMenu(); contextMenu = new ContextMenu();
var openIcon = new FontIcon { Glyph = "\ue71e" };
var header = new MenuItem
{
Header = "Flow Launcher",
IsEnabled = false
};
var open = new MenuItem var open = new MenuItem
{ {
Header = InternationalizationManager.Instance.GetTranslation("iconTrayOpen") + " (" +_settings.Hotkey + ")" Header = InternationalizationManager.Instance.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")",
Icon = openIcon
}; };
var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" };
var gamemode = new MenuItem var gamemode = new MenuItem
{ {
Header = InternationalizationManager.Instance.GetTranslation("GameMode") Header = InternationalizationManager.Instance.GetTranslation("GameMode"),
Icon = gamemodeIcon
}; };
var positionresetIcon = new FontIcon { Glyph = "\ue73f" };
var positionreset = new MenuItem var positionreset = new MenuItem
{ {
Header = InternationalizationManager.Instance.GetTranslation("PositionReset") Header = InternationalizationManager.Instance.GetTranslation("PositionReset"),
Icon = positionresetIcon
}; };
var settingsIcon = new FontIcon { Glyph = "\ue713" };
var settings = new MenuItem var settings = new MenuItem
{ {
Header = InternationalizationManager.Instance.GetTranslation("iconTraySettings") Header = InternationalizationManager.Instance.GetTranslation("iconTraySettings"),
Icon = settingsIcon
}; };
var exitIcon = new FontIcon { Glyph = "\ue7e8" };
var exit = new MenuItem var exit = new MenuItem
{ {
Header = InternationalizationManager.Instance.GetTranslation("iconTrayExit") Header = InternationalizationManager.Instance.GetTranslation("iconTrayExit"),
Icon = exitIcon
}; };
open.Click += (o, e) => _viewModel.ToggleFlowLauncher(); open.Click += (o, e) => _viewModel.ToggleFlowLauncher();
@ -234,7 +247,6 @@ namespace Flow.Launcher
positionreset.Click += (o, e) => PositionReset(); positionreset.Click += (o, e) => PositionReset();
settings.Click += (o, e) => App.API.OpenSettingDialog(); settings.Click += (o, e) => App.API.OpenSettingDialog();
exit.Click += (o, e) => Close(); exit.Click += (o, e) => Close();
contextMenu.Items.Add(header);
contextMenu.Items.Add(open); contextMenu.Items.Add(open);
gamemode.ToolTip = InternationalizationManager.Instance.GetTranslation("GameModeToolTip"); gamemode.ToolTip = InternationalizationManager.Instance.GetTranslation("GameModeToolTip");
positionreset.ToolTip = InternationalizationManager.Instance.GetTranslation("PositionResetToolTip"); positionreset.ToolTip = InternationalizationManager.Instance.GetTranslation("PositionResetToolTip");
@ -375,28 +387,6 @@ namespace Flow.Launcher
if (e.ChangedButton == MouseButton.Left) DragMove(); if (e.ChangedButton == MouseButton.Left) DragMove();
} }
private void OnPreviewMouseButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender != null && e.OriginalSource != null)
{
var r = (ResultListBox)sender;
var d = (DependencyObject)e.OriginalSource;
var item = ItemsControl.ContainerFromElement(r, d) as ListBoxItem;
var result = (ResultViewModel)item?.DataContext;
if (result != null)
{
if (e.ChangedButton == MouseButton.Left)
{
_viewModel.OpenResultCommand.Execute(null);
}
else if (e.ChangedButton == MouseButton.Right)
{
_viewModel.LoadContextMenuCommand.Execute(null);
}
}
}
}
private void OnPreviewDragOver(object sender, DragEventArgs e) private void OnPreviewDragOver(object sender, DragEventArgs e)
{ {
e.Handled = true; e.Handled = true;

View file

@ -1407,7 +1407,8 @@
Padding="{TemplateBinding Padding}" Padding="{TemplateBinding Padding}"
BorderBrush="{DynamicResource ButtonInsideBorder}" BorderBrush="{DynamicResource ButtonInsideBorder}"
BorderThickness="{DynamicResource CustomButtonInsideBorderThickness}" BorderThickness="{DynamicResource CustomButtonInsideBorderThickness}"
CornerRadius="4"> CornerRadius="4"
SnapsToDevicePixels="True">
<ContentPresenter <ContentPresenter
x:Name="ContentPresenter" x:Name="ContentPresenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
@ -2044,31 +2045,43 @@
<Border Padding="{TemplateBinding Padding}"> <Border Padding="{TemplateBinding Padding}">
<Grid Background="Transparent" SnapsToDevicePixels="False"> <Grid Background="Transparent" SnapsToDevicePixels="False">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="19" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Ellipse
x:Name="circle"
Width="19"
Height="19"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stroke="Transparent" />
<Path
x:Name="arrow"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 1,1.5 L 4.5,5 L 8,1.5"
SnapsToDevicePixels="false"
Stroke="#666"
StrokeThickness="1" />
<ContentPresenter <ContentPresenter
Grid.Column="1" Grid.Column="0"
Margin="4,0,0,0" Margin="0,0,0,0"
HorizontalAlignment="Left" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
RecognizesAccessKey="True" RecognizesAccessKey="True"
SnapsToDevicePixels="True" /> SnapsToDevicePixels="True" />
<Grid
x:Name="ChevronGrid"
Grid.Column="1"
Margin="0"
VerticalAlignment="Center"
Background="Transparent"
RenderTransformOrigin="0.5, 0.5">
<Grid.RenderTransform>
<RotateTransform Angle="0" />
</Grid.RenderTransform>
<Ellipse
x:Name="circle"
Width="19"
Height="19"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stroke="Transparent" />
<Path
x:Name="arrow"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 1,1.5 L 4.5,5 L 8,1.5"
SnapsToDevicePixels="false"
Stroke="#666"
StrokeThickness="1" />
</Grid>
</Grid> </Grid>
</Border> </Border>
<ControlTemplate.Triggers> <ControlTemplate.Triggers>
@ -2077,12 +2090,12 @@
</Trigger> </Trigger>
<Trigger Property="IsMouseOver" Value="true"> <Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="circle" Property="Stroke" Value="Transparent" /> <Setter TargetName="circle" Property="Stroke" Value="Transparent" />
<Setter TargetName="arrow" Property="Stroke" Value="#222" /> <Setter TargetName="arrow" Property="Stroke" Value="{DynamicResource Color05B}" />
</Trigger> </Trigger>
<Trigger Property="IsPressed" Value="true"> <Trigger Property="IsPressed" Value="true">
<Setter TargetName="circle" Property="Stroke" Value="Transparent" /> <Setter TargetName="circle" Property="Stroke" Value="Transparent" />
<Setter TargetName="circle" Property="StrokeThickness" Value="1.5" /> <Setter TargetName="circle" Property="StrokeThickness" Value="1.5" />
<Setter TargetName="arrow" Property="Stroke" Value="#FF003366" /> <Setter TargetName="arrow" Property="Stroke" Value="{DynamicResource Color17B}" />
</Trigger> </Trigger>
</ControlTemplate.Triggers> </ControlTemplate.Triggers>
</ControlTemplate> </ControlTemplate>
@ -2110,7 +2123,7 @@
x:Name="HeaderSite" x:Name="HeaderSite"
MinWidth="0" MinWidth="0"
MinHeight="0" MinHeight="0"
Margin="18,0,0,0" Margin="18,0,18,0"
Padding="{TemplateBinding Padding}" Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
@ -2127,19 +2140,62 @@
Foreground="{TemplateBinding Foreground}" Foreground="{TemplateBinding Foreground}"
IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
Style="{StaticResource ExpanderDownHeaderStyle}" /> Style="{StaticResource ExpanderDownHeaderStyle}" />
<ContentPresenter <Border x:Name="ContentPresenterBorder">
x:Name="ExpandSite" <ContentPresenter
Margin="{TemplateBinding Padding}" x:Name="ExpandSite"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
DockPanel.Dock="Bottom" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Focusable="false" DockPanel.Dock="Bottom"
Visibility="Collapsed" /> Focusable="false" />
<Border.LayoutTransform>
<ScaleTransform ScaleY="0" />
</Border.LayoutTransform>
</Border>
</DockPanel> </DockPanel>
</Border> </Border>
<ControlTemplate.Triggers> <ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="true"> <Trigger Property="IsExpanded" Value="true">
<Setter TargetName="ExpandSite" Property="Visibility" Value="Visible" /> <Setter TargetName="ExpandSite" Property="Visibility" Value="Visible" />
<Setter TargetName="ContentPresenterBorder" Property="BorderThickness" Value="0,1,0,0" />
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.LayoutTransform).(ScaleTransform.ScaleY)"
From="0.0"
To="1.0"
Duration="00:00:00.00" />
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.Opacity)"
From="0.0"
To="1.0"
Duration="00:00:00.00" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.LayoutTransform).(ScaleTransform.ScaleY)"
From="1.0"
To="0"
Duration="00:00:00.00" />
<!-- Animation 00:00:00.167 -->
<DoubleAnimation
Storyboard.TargetName="ContentPresenterBorder"
Storyboard.TargetProperty="(Border.Opacity)"
From="1.0"
To="0.0"
Duration="00:00:00.00" />
<!-- Animation 00:00:00.167 -->
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger> </Trigger>
<Trigger Property="ExpandDirection" Value="Right"> <Trigger Property="ExpandDirection" Value="Right">
<Setter TargetName="ExpandSite" Property="DockPanel.Dock" Value="Right" /> <Setter TargetName="ExpandSite" Property="DockPanel.Dock" Value="Right" />
@ -2672,73 +2728,491 @@
</Style> </Style>
<!--#endregion--> <!--#endregion-->
<Style TargetType="{x:Type MenuItem}">
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" /> <system:Double x:Key="MenuBarHeight">40</system:Double>
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" /> <Thickness x:Key="MenuFlyoutScrollerMargin">4,4,4,4</Thickness>
<Setter Property="Background" Value="Transparent" /> <Thickness x:Key="MenuFlyoutItemRevealBorderThickness">1</Thickness>
<Setter Property="Foreground" Value="{DynamicResource Color05B}" /> <Thickness x:Key="ToggleMenuFlyoutItemRevealBorderThickness">1</Thickness>
<Setter Property="Stylus.IsFlicksEnabled" Value="False" /> <Thickness x:Key="MenuFlyoutSubItemRevealBorderThickness">1</Thickness>
<Setter Property="OverridesDefaultStyle" Value="True" />
<ui:SharedSizeGroupConverter x:Key="SharedSizeGroupConverter" />
<Style x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type FrameworkElement}, ResourceId=MenuScrollViewer}" TargetType="ScrollViewer">
<Setter Property="HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="PanningMode" Value="VerticalOnly" />
</Style>
<Style x:Key="DefaultMenuItemSeparatorStyle" TargetType="Separator">
<Setter Property="Background" Value="{DynamicResource MenuFlyoutSeparatorBackground}" />
<Setter Property="Padding" Value="{DynamicResource MenuFlyoutSeparatorThemePadding}" />
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="{x:Type MenuItem}"> <ControlTemplate TargetType="Separator">
<Border <Rectangle
Name="bdr" Height="{DynamicResource MenuFlyoutSeparatorThemeHeight}"
Padding="24,6,24,6" Margin="{TemplateBinding Padding}"
Background="Transparent"> Fill="{TemplateBinding Background}" />
<TextBlock
Name="Menu"
VerticalAlignment="Center"
FontSize="14"
Foreground="{DynamicResource Color05B}"
Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Header}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsVisible" Value="True">
<Setter TargetName="bdr" Property="Background" Value="{DynamicResource CustomContextBackground}" />
<Setter TargetName="Menu" Property="Foreground" Value="{DynamicResource Color05B}" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="bdr" Property="Background" Value="{DynamicResource CustomContextHover}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Menu" Property="Foreground" Value="{DynamicResource CustomContextDisabled}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate> </ControlTemplate>
</Setter.Value> </Setter.Value>
</Setter> </Setter>
</Style> </Style>
<Style TargetType="{x:Type ContextMenu}"> <Style
x:Key="{x:Static MenuItem.SeparatorStyleKey}"
BasedOn="{StaticResource DefaultMenuItemSeparatorStyle}"
TargetType="Separator" />
<ControlTemplate x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=TopLevelItemTemplateKey}" TargetType="{x:Type MenuItem}">
<Grid
x:Name="ContentRoot"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="True">
<Border
x:Name="Background"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" />
<ContentPresenter
Margin="12,0,12,0"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
ContentSource="Header"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Background" Property="Background" Value="{DynamicResource MenuBarItemBackgroundPointerOver}" />
<Setter TargetName="Background" Property="BorderBrush" Value="{DynamicResource MenuBarItemBorderBrushPointerOver}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Background" Property="Background" Value="{DynamicResource MenuBarItemBackgroundPressed}" />
<Setter TargetName="Background" Property="BorderBrush" Value="{DynamicResource MenuBarItemBorderBrushPressed}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<ControlTemplate x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=TopLevelHeaderTemplateKey}" TargetType="{x:Type MenuItem}">
<Grid
x:Name="ContentRoot"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="True">
<Border
x:Name="Background"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" />
<ContentPresenter
Margin="12,0,12,0"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
ContentSource="Header"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
<ui:MenuPopup
x:Name="PART_Popup"
AllowsTransparency="true"
Focusable="false"
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
Placement="Bottom"
PopupAnimation="{DynamicResource {x:Static SystemParameters.MenuPopupAnimationKey}}">
<ui:ThemeShadowChrome CornerRadius="{TemplateBinding ui:ControlHelper.CornerRadius}" IsShadowEnabled="{DynamicResource {x:Static SystemParameters.DropShadowKey}}">
<Border
x:Name="SubMenuRoot"
Background="{DynamicResource MenuFlyoutPresenterBackground}"
CornerRadius="{TemplateBinding ui:ControlHelper.CornerRadius}">
<Grid>
<ScrollViewer
x:Name="SubMenuScrollViewer"
MinWidth="{DynamicResource FlyoutThemeMinWidth}"
Margin="{DynamicResource MenuFlyoutPresenterThemePadding}"
Style="{DynamicResource {ComponentResourceKey ResourceId=MenuScrollViewer,
TypeInTargetAssembly={x:Type FrameworkElement}}}">
<ItemsPresenter
Grid.IsSharedSizeScope="true"
KeyboardNavigation.DirectionalNavigation="Cycle"
KeyboardNavigation.TabNavigation="Cycle"
RenderOptions.ClearTypeHint="Enabled"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ScrollViewer>
<Border
x:Name="SubMenuBorder"
BorderBrush="{DynamicResource MenuFlyoutPresenterBorderBrush}"
BorderThickness="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}"
CornerRadius="{TemplateBinding ui:ControlHelper.CornerRadius}" />
</Grid>
</Border>
</ui:ThemeShadowChrome>
</ui:MenuPopup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSuspendingPopupAnimation" Value="true">
<Setter TargetName="PART_Popup" Property="PopupAnimation" Value="None" />
</Trigger>
<Trigger SourceName="PART_Popup" Property="IsSuspendingAnimation" Value="true">
<Setter TargetName="PART_Popup" Property="PopupAnimation" Value="None" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Background" Property="Background" Value="{DynamicResource MenuBarItemBackgroundPointerOver}" />
<Setter TargetName="Background" Property="BorderBrush" Value="{DynamicResource MenuBarItemBorderBrushPointerOver}" />
</Trigger>
<!-- Selected -->
<Trigger Property="IsSubmenuOpen" Value="True">
<Setter TargetName="Background" Property="Background" Value="{DynamicResource MenuBarItemBackgroundSelected}" />
<Setter TargetName="Background" Property="BorderBrush" Value="{DynamicResource MenuBarItemBorderBrushSelected}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Background" Property="Background" Value="{DynamicResource MenuBarItemBackgroundPressed}" />
<Setter TargetName="Background" Property="BorderBrush" Value="{DynamicResource MenuBarItemBorderBrushPressed}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<!-- DefaultMenuFlyoutSubItemStyle -->
<ControlTemplate x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=SubmenuItemTemplateKey}" TargetType="{x:Type MenuItem}">
<ControlTemplate.Resources>
<StreamGeometry x:Key="CheckMark">F1 M 17.939453 5.439453 L 7.5 15.888672 L 2.060547 10.439453 L 2.939453 9.560547 L 7.5 14.111328 L 17.060547 4.560547 Z</StreamGeometry>
</ControlTemplate.Resources>
<Border
x:Name="LayoutRoot"
Height="Auto"
Margin="5,2,5,2"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding ui:ControlHelper.CornerRadius}"
SnapsToDevicePixels="true"
TextElement.Foreground="{DynamicResource Color05B}"
UseLayoutRounding="True">
<Grid x:Name="AnimationRoot">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="{TemplateBinding Visibility, Converter={StaticResource SharedSizeGroupConverter}, ConverterParameter=MenuItemCheckColumnGroup}" />
<ColumnDefinition Width="Auto" SharedSizeGroup="{TemplateBinding Visibility, Converter={StaticResource SharedSizeGroupConverter}, ConverterParameter=MenuItemIconColumnGroup}" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:FontIconFallback
x:Name="CheckGlyph"
Margin="0,0,16,0"
Data="{StaticResource CheckMark}"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
FontSize="12"
Foreground="{DynamicResource ToggleMenuFlyoutItemCheckGlyphForeground}"
Opacity="0"
Visibility="Collapsed" />
<Viewbox
x:Name="IconRoot"
Grid.Column="1"
Width="16"
Height="16"
Margin="0,1,12,0"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
TextBlock.FontFamily="/Resources/#Segoe Fluent Icons">
<ContentPresenter
x:Name="IconContent"
ContentSource="Icon"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Viewbox>
<ContentPresenter
x:Name="ContentPresenter"
Grid.Column="2"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
ContentSource="Header"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
TextElement.Foreground="{DynamicResource Color05B}" />
<TextBlock
x:Name="KeyboardAcceleratorTextBlock"
Grid.Column="3"
Margin="24,4,0,0"
HorizontalAlignment="Right"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Foreground="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForeground}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{TemplateBinding InputGestureText}"
Visibility="Collapsed" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsVisible" Value="True">
<Setter TargetName="LayoutRoot" Property="TextElement.Foreground" Value="{DynamicResource Color05B}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource Color05B}" />
</Trigger>
<Trigger Property="Icon" Value="{x:Null}">
<Setter TargetName="IconRoot" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="InputGestureText" Value="">
<Setter TargetName="KeyboardAcceleratorTextBlock" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="CheckGlyph" Property="Opacity" Value="1" />
</Trigger>
<Trigger Property="IsCheckable" Value="True">
<Setter TargetName="CheckGlyph" Property="Visibility" Value="Visible" />
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource CustomContextHover}" />
<Setter TargetName="LayoutRoot" Property="TextElement.Foreground" Value="{DynamicResource Color05B}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource Color05B}" />
<Setter TargetName="KeyboardAcceleratorTextBlock" Property="Foreground" Value="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForegroundPointerOver}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource CustomContextClick}" />
<Setter TargetName="LayoutRoot" Property="TextElement.Foreground" Value="{DynamicResource Color05B}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource Color05B}" />
<Setter TargetName="KeyboardAcceleratorTextBlock" Property="Foreground" Value="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForegroundPressed}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource MenuFlyoutItemBackgroundDisabled}" />
<Setter TargetName="LayoutRoot" Property="TextElement.Foreground" Value="{DynamicResource MenuFlyoutItemForegroundDisabled}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource MenuFlyoutItemForegroundDisabled}" />
<Setter TargetName="KeyboardAcceleratorTextBlock" Property="Foreground" Value="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForegroundDisabled}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<!-- DefaultMenuFlyoutItemStyle -->
<ControlTemplate x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=SubmenuHeaderTemplateKey}" TargetType="{x:Type MenuItem}">
<ControlTemplate.Resources>
<StreamGeometry x:Key="ChevronRight">M 5.029297 19.091797 L 14.111328 10 L 5.029297 0.908203 L 5.908203 0.029297 L 15.888672 10 L 5.908203 19.970703 Z</StreamGeometry>
</ControlTemplate.Resources>
<Border
x:Name="LayoutRoot"
Margin="{DynamicResource MenuFlyoutItemMargin}"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding ui:ControlHelper.CornerRadius}"
SnapsToDevicePixels="True">
<Grid x:Name="AnimationRoot">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemCheckColumnGroup" />
<ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemIconColumnGroup" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Viewbox
x:Name="IconRoot"
Grid.Column="1"
Width="16"
Height="16"
Margin="0,0,12,0"
HorizontalAlignment="Left"
VerticalAlignment="Center">
<ContentPresenter
x:Name="IconContent"
ContentSource="Icon"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Viewbox>
<ContentPresenter
x:Name="ContentPresenter"
Grid.Column="2"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
ContentSource="Header"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
TextElement.Foreground="{TemplateBinding Foreground}" />
<ui:FontIconFallback
x:Name="SubItemChevron"
Grid.Column="3"
Margin="{DynamicResource MenuFlyoutItemChevronMargin}"
Data="{StaticResource ChevronRight}"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
FontSize="12"
Foreground="{DynamicResource MenuFlyoutSubItemChevron}">
<UIElement.RenderTransform>
<TranslateTransform Y="1" />
</UIElement.RenderTransform>
</ui:FontIconFallback>
<Popup
x:Name="PART_Popup"
AllowsTransparency="true"
Focusable="false"
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
Placement="Right"
PlacementTarget="{Binding ElementName=LayoutRoot}"
PopupAnimation="{DynamicResource {x:Static SystemParameters.MenuPopupAnimationKey}}">
<Popup.PlacementRectangle>
<MultiBinding>
<MultiBinding.Converter>
<ui:PlacementRectangleConverter Margin="4,-1" />
</MultiBinding.Converter>
<Binding ElementName="LayoutRoot" Path="ActualWidth" />
<Binding ElementName="LayoutRoot" Path="ActualHeight" />
</MultiBinding>
</Popup.PlacementRectangle>
<ui:ThemeShadowChrome CornerRadius="{TemplateBinding ui:ControlHelper.CornerRadius}" IsShadowEnabled="{DynamicResource {x:Static SystemParameters.DropShadowKey}}">
<Border
x:Name="SubMenuRoot"
Background="{DynamicResource MenuFlyoutPresenterBackground}"
CornerRadius="{TemplateBinding ui:ControlHelper.CornerRadius}">
<Grid>
<ScrollViewer
x:Name="SubMenuScrollViewer"
MinWidth="{DynamicResource FlyoutThemeMinWidth}"
Margin="{DynamicResource MenuFlyoutPresenterThemePadding}"
Style="{DynamicResource {ComponentResourceKey ResourceId=MenuScrollViewer,
TypeInTargetAssembly={x:Type FrameworkElement}}}">
<ItemsPresenter
Grid.IsSharedSizeScope="true"
KeyboardNavigation.DirectionalNavigation="Cycle"
KeyboardNavigation.TabNavigation="Cycle"
RenderOptions.ClearTypeHint="Enabled"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ScrollViewer>
<Border
x:Name="SubMenuBorder"
BorderBrush="{DynamicResource MenuFlyoutPresenterBorderBrush}"
BorderThickness="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}"
CornerRadius="{TemplateBinding ui:ControlHelper.CornerRadius}" />
</Grid>
</Border>
</ui:ThemeShadowChrome>
</Popup>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsVisible" Value="True">
<Setter TargetName="LayoutRoot" Property="TextElement.Foreground" Value="{DynamicResource Color05B}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource Color05B}" />
</Trigger>
<Trigger Property="IsSuspendingPopupAnimation" Value="true">
<Setter TargetName="PART_Popup" Property="PopupAnimation" Value="None" />
</Trigger>
<Trigger Property="Icon" Value="{x:Null}">
<Setter TargetName="IconRoot" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource MenuFlyoutSubItemBackgroundPointerOver}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource MenuFlyoutSubItemForegroundPointerOver}" />
<Setter TargetName="SubItemChevron" Property="Foreground" Value="{DynamicResource MenuFlyoutSubItemChevronPointerOver}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource MenuFlyoutSubItemBackgroundPressed}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource MenuFlyoutSubItemForegroundPressed}" />
<Setter TargetName="SubItemChevron" Property="Foreground" Value="{DynamicResource MenuFlyoutSubItemChevronPressed}" />
</Trigger>
<Trigger Property="IsSubmenuOpen" Value="True">
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource MenuFlyoutSubItemBackgroundSubMenuOpened}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource MenuFlyoutSubItemForegroundSubMenuOpened}" />
<Setter TargetName="SubItemChevron" Property="Foreground" Value="{DynamicResource MenuFlyoutSubItemChevronSubMenuOpened}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="LayoutRoot" Property="Background" Value="{DynamicResource MenuFlyoutSubItemBackgroundDisabled}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource MenuFlyoutSubItemForegroundDisabled}" />
<Setter TargetName="SubItemChevron" Property="Foreground" Value="{DynamicResource MenuFlyoutSubItemChevronDisabled}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<Style x:Key="DefaultMenuItemStyle" TargetType="{x:Type MenuItem}">
<Setter Property="OverridesDefaultStyle" Value="True" /> <Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True" /> <Setter Property="Background" Value="{DynamicResource MenuFlyoutItemBackground}" />
<Setter Property="Foreground" Value="{DynamicResource MenuFlyoutItemForeground}" />
<Setter Property="Padding" Value="{DynamicResource MenuFlyoutItemThemePaddingNarrow}" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="ScrollViewer.PanningMode" Value="Both" />
<Setter Property="Stylus.IsFlicksEnabled" Value="False" />
<Setter Property="FocusVisualStyle" Value="{DynamicResource {x:Static SystemParameters.FocusVisualStyleKey}}" />
<Setter Property="ui:FocusVisualHelper.UseSystemFocusVisuals" Value="{DynamicResource UseSystemFocusVisuals}" />
<Setter Property="ui:ControlHelper.CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="Template" Value="{DynamicResource {ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=SubmenuItemTemplateKey}}" />
<Style.Triggers>
<Trigger Property="Role" Value="TopLevelHeader">
<Setter Property="Background" Value="{DynamicResource MenuBarItemBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource MenuBarItemBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource MenuBarItemBorderThickness}" />
<Setter Property="Template" Value="{DynamicResource {ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=TopLevelHeaderTemplateKey}}" />
<Setter Property="IsTabStop" Value="True" />
<Setter Property="Height" Value="{StaticResource MenuBarHeight}" />
</Trigger>
<Trigger Property="Role" Value="TopLevelItem">
<Setter Property="Background" Value="{DynamicResource MenuBarItemBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource MenuBarItemBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource MenuBarItemBorderThickness}" />
<Setter Property="Template" Value="{DynamicResource {ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=TopLevelItemTemplateKey}}" />
<Setter Property="IsTabStop" Value="True" />
<Setter Property="Height" Value="{StaticResource MenuBarHeight}" />
</Trigger>
<Trigger Property="Role" Value="SubmenuHeader">
<Setter Property="Background" Value="{DynamicResource MenuFlyoutSubItemBackground}" />
<Setter Property="Foreground" Value="{DynamicResource MenuFlyoutSubItemForeground}" />
<Setter Property="Template" Value="{DynamicResource {ComponentResourceKey TypeInTargetAssembly={x:Type MenuItem}, ResourceId=SubmenuHeaderTemplateKey}}" />
</Trigger>
<Trigger Property="IsCheckable" Value="True">
<Setter Property="Background" Value="Transparent" />
<Setter Property="ui:FocusVisualHelper.UseSystemFocusVisuals" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
<Style BasedOn="{StaticResource DefaultMenuItemStyle}" TargetType="{x:Type MenuItem}" />
<Style TargetType="{x:Type ContextMenu}">
<Setter Property="Background" Value="{DynamicResource CustomContextBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource CustomContextBorder}" />
<Setter Property="BorderThickness" Value="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}" />
<Setter Property="Padding" Value="{DynamicResource MenuFlyoutPresenterThemePadding}" />
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="FontStyle" Value="Normal" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="Grid.IsSharedSizeScope" Value="true" />
<Setter Property="HasDropShadow" Value="{DynamicResource {x:Static SystemParameters.DropShadowKey}}" />
<!--<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />-->
<!--<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />-->
<!--<Setter Property="ScrollViewer.PanningMode" Value="VerticalOnly" />-->
<Setter Property="Stylus.IsFlicksEnabled" Value="False" />
<Setter Property="MaxWidth" Value="{DynamicResource FlyoutThemeMaxWidth}" />
<Setter Property="MinHeight" Value="{DynamicResource MenuFlyoutThemeMinHeight}" />
<Setter Property="ui:ControlHelper.CornerRadius" Value="{DynamicResource OverlayCornerRadius}" />
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="{x:Type ContextMenu}"> <ControlTemplate TargetType="{x:Type ContextMenu}">
<Border <ui:ThemeShadowChrome
x:Name="Border" x:Name="Shdw"
Margin="5,4,5,12" CornerRadius="{TemplateBinding ui:ControlHelper.CornerRadius}"
Padding="0,4,0,4" IsShadowEnabled="{TemplateBinding HasDropShadow}"
Background="{DynamicResource CustomContextBackground}" SnapsToDevicePixels="True">
BorderBrush="{DynamicResource CustomContextBorder}" <Border
BorderThickness="1" x:Name="Border"
CornerRadius="8" Padding="0,4,0,4"
UseLayoutRounding="True"> Background="{DynamicResource CustomContextBackground}"
<Border.Effect> BorderBrush="{DynamicResource CustomContextBorder}"
<DropShadowEffect BorderThickness="1"
BlurRadius="12" CornerRadius="8">
Direction="-90" <Grid>
Opacity=".15" <ScrollViewer
ShadowDepth="6" x:Name="ContextMenuScrollViewer"
Color="Black" /> MinWidth="{DynamicResource FlyoutThemeMinWidth}"
</Border.Effect> Margin="{TemplateBinding Padding}"
<StackPanel Style="{DynamicResource {ComponentResourceKey ResourceId=MenuScrollViewer,
x:Name="Panel" TypeInTargetAssembly={x:Type FrameworkElement}}}">
ClipToBounds="True" <ItemsPresenter
IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Cycle"
Orientation="Vertical" /> RenderOptions.ClearTypeHint="Enabled"
</Border> SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ScrollViewer>
<Border
x:Name="ContextMenuBorder"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="0"
CornerRadius="{TemplateBinding ui:ControlHelper.CornerRadius}" />
</Grid>
</Border>
</ui:ThemeShadowChrome>
<ControlTemplate.Triggers> <ControlTemplate.Triggers>
<Trigger Property="IsVisible" Value="true"> <Trigger Property="IsVisible" Value="true">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource CustomContextBackground}" /> <Setter TargetName="Border" Property="Background" Value="{DynamicResource CustomContextBackground}" />
@ -2753,4 +3227,7 @@
</Setter.Value> </Setter.Value>
</Setter> </Setter>
</Style> </Style>
<ui:TextContextMenu x:Key="TextControlContextMenu" x:Shared="False" />
</ResourceDictionary> </ResourceDictionary>

View file

@ -65,10 +65,13 @@
<SolidColorBrush x:Key="CustomContextBorder" Color="#1b1b1b" /> <SolidColorBrush x:Key="CustomContextBorder" Color="#1b1b1b" />
<SolidColorBrush x:Key="CustomContextBackground" Color="#2b2b2b" /> <SolidColorBrush x:Key="CustomContextBackground" Color="#2b2b2b" />
<SolidColorBrush x:Key="CustomContextHover" Color="#3c3c3c" /> <SolidColorBrush x:Key="CustomContextHover" Color="#3a3a3a" />
<SolidColorBrush x:Key="CustomContextClick" Color="#353535" />
<SolidColorBrush x:Key="CustomContextDisabled" Color="#868686" /> <SolidColorBrush x:Key="CustomContextDisabled" Color="#868686" />
<SolidColorBrush x:Key="ContextSeparator" Color="#3c3c3c" /> <SolidColorBrush x:Key="ContextSeparator" Color="#3c3c3c" />
<SolidColorBrush x:Key="HoverStoreGrid2">#272727</SolidColorBrush>
<Color x:Key="Color01">#202020</Color> <Color x:Key="Color01">#202020</Color>
<Color x:Key="Color02">#2b2b2b</Color> <Color x:Key="Color02">#2b2b2b</Color>
<Color x:Key="Color03">#1d1d1d</Color> <Color x:Key="Color03">#1d1d1d</Color>

View file

@ -58,10 +58,13 @@
<SolidColorBrush x:Key="CustomContextBorder" Color="#dadada" /> <SolidColorBrush x:Key="CustomContextBorder" Color="#dadada" />
<SolidColorBrush x:Key="CustomContextBackground" Color="#f2f2f2" /> <SolidColorBrush x:Key="CustomContextBackground" Color="#f2f2f2" />
<SolidColorBrush x:Key="CustomContextHover" Color="#DBDBDB" /> <SolidColorBrush x:Key="CustomContextHover" Color="#e4e4e4" />
<SolidColorBrush x:Key="CustomContextClick" Color="#ececec" />
<SolidColorBrush x:Key="CustomContextDisabled" Color="#868686" /> <SolidColorBrush x:Key="CustomContextDisabled" Color="#868686" />
<SolidColorBrush x:Key="ContextSeparator" Color="#dadada" /> <SolidColorBrush x:Key="ContextSeparator" Color="#dadada" />
<SolidColorBrush x:Key="HoverStoreGrid2">#f6f6f6</SolidColorBrush>
<Color x:Key="Color01">#f3f3f3</Color> <Color x:Key="Color01">#f3f3f3</Color>
<Color x:Key="Color02">#ffffff</Color> <Color x:Key="Color02">#ffffff</Color>
<Color x:Key="Color03">#e5e5e5</Color> <Color x:Key="Color03">#e5e5e5</Color>

View file

@ -25,12 +25,17 @@
VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Standard" VirtualizingStackPanel.VirtualizationMode="Standard"
Visibility="{Binding Visbility}" Visibility="{Binding Visbility}"
mc:Ignorable="d"> mc:Ignorable="d"
PreviewMouseMove="ResultList_MouseMove"
PreviewMouseLeftButtonDown="ResultList_PreviewMouseLeftButtonDown"
PreviewMouseLeftButtonUp="ResultListBox_OnPreviewMouseUp"
PreviewMouseRightButtonDown="ResultListBox_OnPreviewMouseRightButtonDown">
<!-- IsSynchronizedWithCurrentItem: http://stackoverflow.com/a/7833798/2833083 --> <!-- IsSynchronizedWithCurrentItem: http://stackoverflow.com/a/7833798/2833083 -->
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<Button HorizontalAlignment="Stretch"> <Button
HorizontalAlignment="Stretch">
<Button.Template> <Button.Template>
<ControlTemplate> <ControlTemplate>
<ContentPresenter Content="{TemplateBinding Button.Content}" /> <ContentPresenter Content="{TemplateBinding Button.Content}" />
@ -84,7 +89,7 @@
x:Name="ImageIcon" x:Name="ImageIcon"
Width="{Binding IconXY}" Width="{Binding IconXY}"
Height="{Binding IconXY}" Height="{Binding IconXY}"
Margin="0,0,0,0" Margin="0,0,0,0" IsHitTestVisible="False"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Source="{Binding Image, TargetNullValue={x:Null}}" Source="{Binding Image, TargetNullValue={x:Null}}"
Stretch="Uniform" Stretch="Uniform"
@ -136,7 +141,9 @@
DockPanel.Dock="Left" DockPanel.Dock="Left"
Style="{DynamicResource ItemTitleStyle}" Style="{DynamicResource ItemTitleStyle}"
Text="{Binding Result.Title}" Text="{Binding Result.Title}"
ToolTip="{Binding ShowTitleToolTip}"> ToolTip="{Binding ShowTitleToolTip}"
ToolTipService.ShowOnDisabled="True"
IsEnabled="False">
<vm:ResultsViewModel.FormattedText> <vm:ResultsViewModel.FormattedText>
<MultiBinding Converter="{StaticResource HighlightTextConverter}"> <MultiBinding Converter="{StaticResource HighlightTextConverter}">
<Binding Path="Result.Title" /> <Binding Path="Result.Title" />
@ -147,10 +154,11 @@
<TextBlock <TextBlock
x:Name="SubTitle" x:Name="SubTitle"
Grid.Row="1" Grid.Row="1"
IsEnabled="False"
ToolTipService.ShowOnDisabled="True"
Style="{DynamicResource ItemSubTitleStyle}" Style="{DynamicResource ItemSubTitleStyle}"
Text="{Binding Result.SubTitle}" Text="{Binding Result.SubTitle}"
ToolTip="{Binding ShowSubTitleToolTip}" /> ToolTip="{Binding ShowSubTitleToolTip}"/>
</Grid> </Grid>
</Grid> </Grid>

View file

@ -1,6 +1,9 @@
using System.Windows; using System;
using System.IO;
using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using Flow.Launcher.ViewModel;
namespace Flow.Launcher namespace Flow.Launcher
{ {
@ -14,6 +17,36 @@ namespace Flow.Launcher
InitializeComponent(); InitializeComponent();
} }
public static readonly DependencyProperty RightClickResultCommandProperty =
DependencyProperty.Register("RightClickResultCommand", typeof(ICommand), typeof(ResultListBox), new UIPropertyMetadata(null));
public ICommand RightClickResultCommand
{
get
{
return (ICommand)GetValue(RightClickResultCommandProperty);
}
set
{
SetValue(RightClickResultCommandProperty, value);
}
}
public static readonly DependencyProperty LeftClickResultCommandProperty =
DependencyProperty.Register("LeftClickResultCommand", typeof(ICommand), typeof(ResultListBox), new UIPropertyMetadata(null));
public ICommand LeftClickResultCommand
{
get
{
return (ICommand)GetValue(LeftClickResultCommandProperty);
}
set
{
SetValue(LeftClickResultCommandProperty, value);
}
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{ {
if (e.AddedItems.Count > 0 && e.AddedItems[0] != null) if (e.AddedItems.Count > 0 && e.AddedItems[0] != null)
@ -24,7 +57,7 @@ namespace Flow.Launcher
private void OnMouseEnter(object sender, MouseEventArgs e) private void OnMouseEnter(object sender, MouseEventArgs e)
{ {
lock(_lock) lock (_lock)
{ {
curItem = (ListBoxItem)sender; curItem = (ListBoxItem)sender;
var p = e.GetPosition((IInputElement)sender); var p = e.GetPosition((IInputElement)sender);
@ -34,7 +67,7 @@ namespace Flow.Launcher
private void OnMouseMove(object sender, MouseEventArgs e) private void OnMouseMove(object sender, MouseEventArgs e)
{ {
lock(_lock) lock (_lock)
{ {
var p = e.GetPosition((IInputElement)sender); var p = e.GetPosition((IInputElement)sender);
if (_lastpos != p) if (_lastpos != p)
@ -46,7 +79,7 @@ namespace Flow.Launcher
private void ListBox_PreviewMouseDown(object sender, MouseButtonEventArgs e) private void ListBox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{ {
lock(_lock) lock (_lock)
{ {
if (curItem != null) if (curItem != null)
{ {
@ -54,5 +87,65 @@ namespace Flow.Launcher
} }
} }
} }
private Point start;
private string path;
private string query;
private void ResultList_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (Mouse.DirectlyOver is not FrameworkElement { DataContext: ResultViewModel result })
return;
path = result.Result.CopyText;
query = result.Result.OriginQuery.RawQuery;
start = e.GetPosition(null);
}
private void ResultList_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
{
start = default;
path = string.Empty;
query = string.Empty;
return;
}
if (!File.Exists(path) && !Directory.Exists(path))
return;
Point mousePosition = e.GetPosition(null);
Vector diff = this.start - mousePosition;
if (Math.Abs(diff.X) < SystemParameters.MinimumHorizontalDragDistance
|| Math.Abs(diff.Y) < SystemParameters.MinimumVerticalDragDistance)
return;
var data = new DataObject(DataFormats.FileDrop, new[]
{
path
});
DragDrop.DoDragDrop((DependencyObject)sender, data, DragDropEffects.Move | DragDropEffects.Copy);
App.API.ChangeQuery(query, true);
e.Handled = true;
}
private void ResultListBox_OnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
if (Mouse.DirectlyOver is not FrameworkElement { DataContext: ResultViewModel result })
return;
RightClickResultCommand?.Execute(result.Result);
}
private void ResultListBox_OnPreviewMouseUp(object sender, MouseButtonEventArgs e)
{
if (Mouse.DirectlyOver is not FrameworkElement { DataContext: ResultViewModel result })
return;
LeftClickResultCommand?.Execute(null);
}
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -8,14 +8,19 @@ using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.ViewModel; using Flow.Launcher.ViewModel;
using ModernWpf; using ModernWpf;
using ModernWpf.Controls;
using System; using System;
using System.Drawing.Printing;
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Forms; using System.Windows.Forms;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Interop; using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Navigation; using System.Windows.Navigation;
using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window;
using Button = System.Windows.Controls.Button; using Button = System.Windows.Controls.Button;
using Control = System.Windows.Controls.Control; using Control = System.Windows.Controls.Control;
using KeyEventArgs = System.Windows.Input.KeyEventArgs; using KeyEventArgs = System.Windows.Input.KeyEventArgs;
@ -39,10 +44,6 @@ namespace Flow.Launcher
API = api; API = api;
InitializePosition(); InitializePosition();
InitializeComponent(); InitializeComponent();
CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(StoreListBox.ItemsSource);
PropertyGroupDescription groupDescription = new PropertyGroupDescription("Category");
view.GroupDescriptions.Add(groupDescription);
} }
#region General #region General
@ -154,7 +155,7 @@ namespace Flow.Launcher
} }
} }
private void OnnEditCustomHotkeyClick(object sender, RoutedEventArgs e) private void OnEditCustomHotkeyClick(object sender, RoutedEventArgs e)
{ {
var item = viewModel.SelectedCustomPluginHotkey; var item = viewModel.SelectedCustomPluginHotkey;
if (item != null) if (item != null)
@ -252,6 +253,7 @@ namespace Flow.Launcher
private void OnClosed(object sender, EventArgs e) private void OnClosed(object sender, EventArgs e)
{ {
settings.SettingWindowState = WindowState;
settings.SettingWindowTop = Top; settings.SettingWindowTop = Top;
settings.SettingWindowLeft = Left; settings.SettingWindowLeft = Left;
viewModel.Save(); viewModel.Save();
@ -296,17 +298,35 @@ namespace Flow.Launcher
} }
} }
private void OnPluginStoreRefreshClick(object sender, RoutedEventArgs e) private static T FindParent<T>(DependencyObject child) where T : DependencyObject
{ {
_ = viewModel.RefreshExternalPluginsAsync(); //get parent item
} DependencyObject parentObject = VisualTreeHelper.GetParent(child);
//we've reached the end of the tree
if (parentObject == null) return null;
//check if the parent matches the type we're looking for
T parent = parentObject as T;
if (parent != null)
return parent;
else
return FindParent<T>(parentObject);
}
private void OnExternalPluginInstallClick(object sender, RoutedEventArgs e) private void OnExternalPluginInstallClick(object sender, RoutedEventArgs e)
{ {
if (sender is Button { DataContext: PluginStoreItemViewModel plugin }) if (sender is not Button { DataContext: PluginStoreItemViewModel plugin } button)
{ {
viewModel.DisplayPluginQuery($"install {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")); return;
} }
if (storeClickedButton != null)
{
FlyoutService.GetFlyout(storeClickedButton).Hide();
}
viewModel.DisplayPluginQuery($"install {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7"));
} }
private void OnExternalPluginUninstallClick(object sender, MouseButtonEventArgs e) private void OnExternalPluginUninstallClick(object sender, MouseButtonEventArgs e)
@ -317,18 +337,30 @@ namespace Flow.Launcher
viewModel.DisplayPluginQuery($"uninstall {name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")); viewModel.DisplayPluginQuery($"uninstall {name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7"));
} }
} }
private void OnExternalPluginUninstallClick(object sender, RoutedEventArgs e) private void OnExternalPluginUninstallClick(object sender, RoutedEventArgs e)
{ {
if (storeClickedButton != null)
{
FlyoutService.GetFlyout(storeClickedButton).Hide();
}
if (sender is Button { DataContext: PluginStoreItemViewModel plugin }) if (sender is Button { DataContext: PluginStoreItemViewModel plugin })
viewModel.DisplayPluginQuery($"uninstall {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")); viewModel.DisplayPluginQuery($"uninstall {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7"));
} }
private void OnExternalPluginUpdateClick(object sender, RoutedEventArgs e) private void OnExternalPluginUpdateClick(object sender, RoutedEventArgs e)
{ {
if (storeClickedButton != null)
{
FlyoutService.GetFlyout(storeClickedButton).Hide();
}
if (sender is Button { DataContext: PluginStoreItemViewModel plugin }) if (sender is Button { DataContext: PluginStoreItemViewModel plugin })
viewModel.DisplayPluginQuery($"update {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7")); viewModel.DisplayPluginQuery($"update {plugin.Name}", PluginManager.GetPluginForId("9f8f9b14-2518-4907-b211-35ab6290dee7"));
} }
private void window_MouseDown(object sender, MouseButtonEventArgs e) /* for close hotkey popup */ private void window_MouseDown(object sender, MouseButtonEventArgs e) /* for close hotkey popup */
@ -381,11 +413,34 @@ namespace Flow.Launcher
restoreButton.Visibility = Visibility.Collapsed; restoreButton.Visibility = Visibility.Collapsed;
} }
} }
private void Window_StateChanged(object sender, EventArgs e) private void Window_StateChanged(object sender, EventArgs e)
{ {
RefreshMaximizeRestoreButton(); RefreshMaximizeRestoreButton();
} }
#region Shortcut
private void OnDeleteCustomShortCutClick(object sender, RoutedEventArgs e)
{
viewModel.DeleteSelectedCustomShortcut();
}
private void OnEditCustomShortCutClick(object sender, RoutedEventArgs e)
{
if (viewModel.EditSelectedCustomShortcut())
{
customShortcutView.Items.Refresh();
}
}
private void OnAddCustomShortCutClick(object sender, RoutedEventArgs e)
{
viewModel.AddCustomShortcut();
}
#endregion
private CollectionView pluginListView; private CollectionView pluginListView;
private CollectionView pluginStoreView; private CollectionView pluginStoreView;
@ -463,6 +518,7 @@ namespace Flow.Launcher
{ {
ClockDisplay(); ClockDisplay();
} }
public void ClockDisplay() public void ClockDisplay()
{ {
if (settings.UseClock) if (settings.UseClock)
@ -498,6 +554,7 @@ namespace Flow.Launcher
Top = WindowTop(); Top = WindowTop();
Left = WindowLeft(); Left = WindowLeft();
} }
WindowState = settings.SettingWindowState;
} }
public double WindowLeft() public double WindowLeft()
{ {
@ -517,5 +574,21 @@ namespace Flow.Launcher
return top; return top;
} }
private Button storeClickedButton;
private void StoreListItem_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button button)
return;
storeClickedButton = button;
var flyout = FlyoutService.GetFlyout(button);
flyout.Closed += (_, _) =>
{
storeClickedButton = null;
};
}
} }
} }

View file

@ -17,6 +17,7 @@ using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Storage; using Flow.Launcher.Storage;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Microsoft.VisualStudio.Threading; using Microsoft.VisualStudio.Threading;
using System.Text;
using System.Threading.Channels; using System.Threading.Channels;
using ISavable = Flow.Launcher.Plugin.ISavable; using ISavable = Flow.Launcher.Plugin.ISavable;
using System.IO; using System.IO;
@ -76,12 +77,21 @@ namespace Flow.Launcher.ViewModel
_userSelectedRecord = _userSelectedRecordStorage.Load(); _userSelectedRecord = _userSelectedRecordStorage.Load();
_topMostRecord = _topMostRecordStorage.Load(); _topMostRecord = _topMostRecordStorage.Load();
ContextMenu = new ResultsViewModel(Settings);
Results = new ResultsViewModel(Settings); ContextMenu = new ResultsViewModel(Settings)
History = new ResultsViewModel(Settings); {
LeftClickResultCommand = OpenResultCommand, RightClickResultCommand = LoadContextMenuCommand
};
Results = new ResultsViewModel(Settings)
{
LeftClickResultCommand = OpenResultCommand, RightClickResultCommand = LoadContextMenuCommand
};
History = new ResultsViewModel(Settings)
{
LeftClickResultCommand = OpenResultCommand, RightClickResultCommand = LoadContextMenuCommand
};
_selectedResults = Results; _selectedResults = Results;
InitializeKeyCommands();
RegisterViewUpdate(); RegisterViewUpdate();
RegisterResultsUpdatedEvent(); RegisterResultsUpdatedEvent();
@ -156,165 +166,164 @@ namespace Flow.Launcher.ViewModel
} }
} }
[RelayCommand]
private async Task ReloadPluginDataAsync()
private void InitializeKeyCommands()
{ {
EscCommand = new RelayCommand(_ => Hide();
await PluginManager.ReloadDataAsync().ConfigureAwait(false);
Notification.Show(InternationalizationManager.Instance.GetTranslation("success"), InternationalizationManager.Instance.GetTranslation("completedSuccessfully"));
}
[RelayCommand]
private void LoadHistory()
{
if (SelectedIsFromQueryResults())
{ {
if (!SelectedIsFromQueryResults()) SelectedResults = History;
{ History.SelectedIndex = _history.Items.Count - 1;
SelectedResults = Results; }
} else
else
{
Hide();
}
});
ClearQueryCommand = new RelayCommand(_ =>
{ {
if (!string.IsNullOrEmpty(QueryText)) SelectedResults = Results;
{ }
ChangeQueryText(string.Empty); }
[RelayCommand]
// Push Event to UI SystemQuery has changed private void LoadContextMenu()
//OnPropertyChanged(nameof(SystemQueryText)); {
} if (SelectedIsFromQueryResults())
});
SelectNextItemCommand = new RelayCommand(_ => { SelectedResults.SelectNextResult(); });
SelectPrevItemCommand = new RelayCommand(_ => { SelectedResults.SelectPrevResult(); });
SelectNextPageCommand = new RelayCommand(_ => { SelectedResults.SelectNextPage(); });
SelectPrevPageCommand = new RelayCommand(_ => { SelectedResults.SelectPrevPage(); });
SelectFirstResultCommand = new RelayCommand(_ => SelectedResults.SelectFirstResult());
StartHelpCommand = new RelayCommand(_ =>
{ {
PluginManager.API.OpenUrl("https://github.com/Flow-Launcher/Flow.Launcher/wiki/Flow-Launcher/"); // When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing
}); // i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing
OpenSettingCommand = new RelayCommand(_ => { App.API.OpenSettingDialog(); }); if (SelectedResults.SelectedItem != null)
OpenResultCommand = new RelayCommand(async index => SelectedResults = ContextMenu;
}
else
{ {
var results = SelectedResults; SelectedResults = Results;
}
}
[RelayCommand]
private void Backspace(object index)
{
var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins);
if (index != null) // GetPreviousExistingDirectory does not require trailing '\', otherwise will return empty string
var path = FilesFolders.GetPreviousExistingDirectory((_) => true, query.Search.TrimEnd('\\'));
var actionKeyword = string.IsNullOrEmpty(query.ActionKeyword) ? string.Empty : $"{query.ActionKeyword} ";
ChangeQueryText($"{actionKeyword}{path}");
}
[RelayCommand]
private void AutocompleteQuery()
{
var result = SelectedResults.SelectedItem?.Result;
if (result != null && SelectedIsFromQueryResults()) // SelectedItem returns null if selection is empty.
{
var autoCompleteText = result.Title;
if (!string.IsNullOrEmpty(result.AutoCompleteText))
{ {
results.SelectedIndex = int.Parse(index.ToString()!); autoCompleteText = result.AutoCompleteText;
}
else if (!string.IsNullOrEmpty(SelectedResults.SelectedItem?.QuerySuggestionText))
{
autoCompleteText = SelectedResults.SelectedItem.QuerySuggestionText;
} }
var result = results.SelectedItem?.Result; var specialKeyState = GlobalHotkey.CheckModifiers();
if (result == null) if (specialKeyState.ShiftPressed)
{ {
return; autoCompleteText = result.SubTitle;
} }
var hideWindow = await result.ExecuteAsync(new ActionContext
ChangeQueryText(autoCompleteText);
}
}
[RelayCommand]
private async Task OpenResultAsync(string index)
{
var results = SelectedResults;
if (index is not null)
{
results.SelectedIndex = int.Parse(index);
}
var result = results.SelectedItem?.Result;
if (result == null)
{
return;
}
var hideWindow = await result.ExecuteAsync(new ActionContext
{ {
SpecialKeyState = GlobalHotkey.CheckModifiers() SpecialKeyState = GlobalHotkey.CheckModifiers()
}).ConfigureAwait(false); })
.ConfigureAwait(false);
if (hideWindow) if (hideWindow)
{
Hide();
}
if (SelectedIsFromQueryResults())
{
_userSelectedRecord.Add(result);
_history.Add(result.OriginQuery.RawQuery);
}
else
{
SelectedResults = Results;
}
});
AutocompleteQueryCommand = new RelayCommand(_ =>
{
var result = SelectedResults.SelectedItem?.Result;
if (result != null && SelectedIsFromQueryResults()) // SelectedItem returns null if selection is empty.
{
var autoCompleteText = result.Title;
if (!string.IsNullOrEmpty(result.AutoCompleteText))
{
autoCompleteText = result.AutoCompleteText;
}
else if (!string.IsNullOrEmpty(SelectedResults.SelectedItem?.QuerySuggestionText))
{
autoCompleteText = SelectedResults.SelectedItem.QuerySuggestionText;
}
var specialKeyState = GlobalHotkey.CheckModifiers();
if (specialKeyState.ShiftPressed)
{
autoCompleteText = result.SubTitle;
}
ChangeQueryText(autoCompleteText);
}
});
BackspaceCommand = new RelayCommand(index =>
{
var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins);
// GetPreviousExistingDirectory does not require trailing '\', otherwise will return empty string
var path = FilesFolders.GetPreviousExistingDirectory((_) => true, query.Search.TrimEnd('\\'));
var actionKeyword = string.IsNullOrEmpty(query.ActionKeyword) ? string.Empty : $"{query.ActionKeyword} ";
ChangeQueryText($"{actionKeyword}{path}");
});
LoadContextMenuCommand = new RelayCommand(_ =>
{
if (SelectedIsFromQueryResults())
{
// When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing
// i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing
if (SelectedResults.SelectedItem != null)
SelectedResults = ContextMenu;
}
else
{
SelectedResults = Results;
}
});
LoadHistoryCommand = new RelayCommand(_ =>
{
if (SelectedIsFromQueryResults())
{
SelectedResults = History;
History.SelectedIndex = _history.Items.Count - 1;
}
else
{
SelectedResults = Results;
}
});
ReloadPluginDataCommand = new RelayCommand(_ =>
{ {
Hide(); Hide();
}
_ = PluginManager if (SelectedIsFromQueryResults())
.ReloadDataAsync() {
.ContinueWith(_ => _userSelectedRecord.Add(result);
Application.Current.Dispatcher.Invoke(() => _history.Add(result.OriginQuery.RawQuery);
{ }
Notification.Show( else
InternationalizationManager.Instance.GetTranslation("success"), {
InternationalizationManager.Instance.GetTranslation("completedSuccessfully") SelectedResults = Results;
); }
}), TaskScheduler.Default) }
.ConfigureAwait(false); [RelayCommand]
}); private void OpenSetting()
{
App.API.OpenSettingDialog();
}
[RelayCommand]
private void SelectHelp()
{
PluginManager.API.OpenUrl("https://www.flowlauncher.com/docs/#/usage-tips");
}
[RelayCommand]
private void SelectFirstResult()
{
SelectedResults.SelectFirstResult();
}
[RelayCommand]
private void SelectPrevPage()
{
SelectedResults.SelectPrevPage();
}
[RelayCommand]
private void SelectNextPage()
{
SelectedResults.SelectNextPage();
}
[RelayCommand]
private void SelectPrevItem()
{
SelectedResults.SelectPrevResult();
}
[RelayCommand]
private void SelectNextItem()
{
SelectedResults.SelectNextResult();
}
[RelayCommand]
private void Esc()
{
if (!SelectedIsFromQueryResults())
{
SelectedResults = Results;
}
else
{
Hide();
}
} }
#endregion #endregion
@ -362,9 +371,9 @@ namespace Flow.Launcher.ViewModel
{ {
if (MainWindowWidth + 100 > 1920 || Settings.WindowSize == 1920) if (MainWindowWidth + 100 > 1920 || Settings.WindowSize == 1920)
{ {
Settings.WindowSize = 1920; Settings.WindowSize = 1920;
} }
else else
{ {
Settings.WindowSize += 100; Settings.WindowSize += 100;
Settings.WindowLeft -= 50; Settings.WindowLeft -= 50;
@ -410,6 +419,7 @@ namespace Flow.Launcher.ViewModel
/// but we don't want to move cursor to end when query is updated from TextBox /// but we don't want to move cursor to end when query is updated from TextBox
/// </summary> /// </summary>
/// <param name="queryText"></param> /// <param name="queryText"></param>
/// <param name="reQuery">Force query even when Query Text doesn't change</param>
public void ChangeQueryText(string queryText, bool reQuery = false) public void ChangeQueryText(string queryText, bool reQuery = false)
{ {
if (QueryText != queryText) if (QueryText != queryText)
@ -488,25 +498,6 @@ namespace Flow.Launcher.ViewModel
public string PluginIconPath { get; set; } = null; public string PluginIconPath { get; set; } = null;
public ICommand EscCommand { get; set; }
public ICommand BackspaceCommand { get; set; }
public ICommand SelectNextItemCommand { get; set; }
public ICommand SelectPrevItemCommand { get; set; }
public ICommand SelectNextPageCommand { get; set; }
public ICommand SelectPrevPageCommand { get; set; }
public ICommand SelectFirstResultCommand { get; set; }
public ICommand StartHelpCommand { get; set; }
public ICommand LoadContextMenuCommand { get; set; }
public ICommand LoadHistoryCommand { get; set; }
public ICommand OpenResultCommand { get; set; }
public ICommand OpenSettingCommand { get; set; }
public ICommand ReloadPluginDataCommand { get; set; }
public ICommand ClearQueryCommand { get; private set; }
public ICommand CopyToClipboard { get; set; }
public ICommand AutocompleteQueryCommand { get; set; }
public string OpenResultCommandModifiers { get; private set; } public string OpenResultCommandModifiers { get; private set; }
public string Image => Constant.QueryTextBoxIconImagePath; public string Image => Constant.QueryTextBoxIconImagePath;
@ -621,7 +612,9 @@ namespace Flow.Launcher.ViewModel
{ {
_updateSource?.Cancel(); _updateSource?.Cancel();
if (string.IsNullOrWhiteSpace(QueryText)) var query = ConstructQuery(QueryText, Settings.CustomShortcuts, Settings.BuiltinShortcuts);
if (query == null) // shortcut expanded
{ {
Results.Clear(); Results.Clear();
Results.Visbility = Visibility.Collapsed; Results.Visbility = Visibility.Collapsed;
@ -646,7 +639,6 @@ namespace Flow.Launcher.ViewModel
if (currentCancellationToken.IsCancellationRequested) if (currentCancellationToken.IsCancellationRequested)
return; return;
var query = QueryBuilder.Build(QueryText, PluginManager.NonGlobalPlugins);
// handle the exclusiveness of plugin using action keyword // handle the exclusiveness of plugin using action keyword
RemoveOldQueryResults(query); RemoveOldQueryResults(query);
@ -736,6 +728,40 @@ namespace Flow.Launcher.ViewModel
} }
} }
private Query ConstructQuery(string queryText, IEnumerable<CustomShortcutModel> customShortcuts, IEnumerable<BuiltinShortcutModel> builtInShortcuts)
{
if (string.IsNullOrWhiteSpace(queryText))
{
return null;
}
StringBuilder queryBuilder = new(queryText);
StringBuilder queryBuilderTmp = new(queryText);
foreach (var shortcut in customShortcuts)
{
if (queryBuilder.Equals(shortcut.Key))
{
queryBuilder.Replace(shortcut.Key, shortcut.Expand());
}
queryBuilder.Replace('@' + shortcut.Key, shortcut.Expand());
}
foreach (var shortcut in builtInShortcuts)
{
queryBuilder.Replace(shortcut.Key, shortcut.Expand());
queryBuilderTmp.Replace(shortcut.Key, shortcut.Expand());
}
// show expanded builtin shortcuts
// use private field to avoid infinite recursion
_queryText = queryBuilderTmp.ToString();
var query = QueryBuilder.Build(queryBuilder.ToString().Trim(), PluginManager.NonGlobalPlugins);
return query;
}
private void RemoveOldQueryResults(Query query) private void RemoveOldQueryResults(Query query)
{ {
if (_lastQuery?.ActionKeyword != query?.ActionKeyword) if (_lastQuery?.ActionKeyword != query?.ActionKeyword)
@ -966,28 +992,26 @@ namespace Flow.Launcher.ViewModel
var result = Results.SelectedItem?.Result; var result = Results.SelectedItem?.Result;
if (result != null) if (result != null)
{ {
string copyText = string.IsNullOrEmpty(result.CopyText) ? result.SubTitle : result.CopyText; string copyText = result.CopyText;
var isFile = File.Exists(copyText); var isFile = File.Exists(copyText);
var isFolder = Directory.Exists(copyText); var isFolder = Directory.Exists(copyText);
if (isFile || isFolder) if (isFile || isFolder)
{ {
var paths = new StringCollection(); var paths = new StringCollection
paths.Add(copyText); {
copyText
};
Clipboard.SetFileDropList(paths); Clipboard.SetFileDropList(paths);
App.API.ShowMsg( App.API.ShowMsg(
App.API.GetTranslation("copy") $"{App.API.GetTranslation("copy")} {(isFile ? App.API.GetTranslation("fileTitle") : App.API.GetTranslation("folderTitle"))}",
+ " "
+ (isFile ? App.API.GetTranslation("fileTitle") : App.API.GetTranslation("folderTitle")),
App.API.GetTranslation("completedSuccessfully")); App.API.GetTranslation("completedSuccessfully"));
} }
else else
{ {
Clipboard.SetDataObject(copyText.ToString()); Clipboard.SetDataObject(copyText);
App.API.ShowMsg( App.API.ShowMsg(
App.API.GetTranslation("copy") $"{App.API.GetTranslation("copy")} {App.API.GetTranslation("textTitle")}",
+ " "
+ App.API.GetTranslation("textTitle"),
App.API.GetTranslation("completedSuccessfully")); App.API.GetTranslation("completedSuccessfully"));
} }
} }

View file

@ -8,6 +8,8 @@ using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Documents; using System.Windows.Documents;
using System.Windows.Input;
using JetBrains.Annotations;
namespace Flow.Launcher.ViewModel namespace Flow.Launcher.ViewModel
{ {
@ -49,6 +51,9 @@ namespace Flow.Launcher.ViewModel
public ResultViewModel SelectedItem { get; set; } public ResultViewModel SelectedItem { get; set; }
public Thickness Margin { get; set; } public Thickness Margin { get; set; }
public Visibility Visbility { get; set; } = Visibility.Collapsed; public Visibility Visbility { get; set; } = Visibility.Collapsed;
public ICommand RightClickResultCommand { get; init; }
public ICommand LeftClickResultCommand { get; init; }
#endregion #endregion

View file

@ -19,10 +19,12 @@ using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.Plugin.SharedModels;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.Input;
namespace Flow.Launcher.ViewModel namespace Flow.Launcher.ViewModel
{ {
public class SettingWindowViewModel : BaseModel public partial class SettingWindowViewModel : BaseModel
{ {
private readonly Updater _updater; private readonly Updater _updater;
private readonly IPortable _portable; private readonly IPortable _portable;
@ -212,11 +214,19 @@ namespace Flow.Launcher.ViewModel
} }
} }
public List<string> OpenResultModifiersList => new List<string> { KeyConstant.Alt, KeyConstant.Ctrl, $"{KeyConstant.Ctrl}+{KeyConstant.Alt}" }; public List<string> OpenResultModifiersList => new List<string>
{
KeyConstant.Alt,
KeyConstant.Ctrl,
$"{KeyConstant.Ctrl}+{KeyConstant.Alt}"
};
private Internationalization _translater => InternationalizationManager.Instance; private Internationalization _translater => InternationalizationManager.Instance;
public List<Language> Languages => _translater.LoadAvailableLanguages(); public List<Language> Languages => _translater.LoadAvailableLanguages();
public IEnumerable<int> MaxResultsRange => Enumerable.Range(2, 16); public IEnumerable<int> MaxResultsRange => Enumerable.Range(2, 16);
public ObservableCollection<CustomShortcutModel> CustomShortcuts => Settings.CustomShortcuts;
public ObservableCollection<BuiltinShortcutModel> BuiltinShortcuts => Settings.BuiltinShortcuts;
public string TestProxy() public string TestProxy()
{ {
var proxyServer = Settings.Proxy.Server; var proxyServer = Settings.Proxy.Server;
@ -275,7 +285,10 @@ namespace Flow.Launcher.ViewModel
var metadatas = PluginManager.AllPlugins var metadatas = PluginManager.AllPlugins
.OrderBy(x => x.Metadata.Disabled) .OrderBy(x => x.Metadata.Disabled)
.ThenBy(y => y.Metadata.Name) .ThenBy(y => y.Metadata.Name)
.Select(p => new PluginViewModel { PluginPair = p }) .Select(p => new PluginViewModel
{
PluginPair = p
})
.ToList(); .ToList();
return metadatas; return metadatas;
} }
@ -318,7 +331,8 @@ namespace Flow.Launcher.ViewModel
} }
} }
public async Task RefreshExternalPluginsAsync() [RelayCommand]
private async Task RefreshExternalPluginsAsync()
{ {
await PluginsManifest.UpdateManifestAsync(); await PluginsManifest.UpdateManifestAsync();
OnPropertyChanged(nameof(ExternalPlugins)); OnPropertyChanged(nameof(ExternalPlugins));
@ -397,7 +411,11 @@ namespace Flow.Launcher.ViewModel
{ {
var key = $"ColorScheme{e}"; var key = $"ColorScheme{e}";
var display = _translater.GetTranslation(key); var display = _translater.GetTranslation(key);
var m = new ColorScheme { Display = display, Value = e, }; var m = new ColorScheme
{
Display = display,
Value = e,
};
modes.Add(m); modes.Add(m);
} }
return modes; return modes;
@ -519,8 +537,13 @@ namespace Flow.Launcher.ViewModel
var bitmap = new BitmapImage(); var bitmap = new BitmapImage();
bitmap.BeginInit(); bitmap.BeginInit();
bitmap.StreamSource = memStream; bitmap.StreamSource = memStream;
bitmap.DecodePixelWidth = 800;
bitmap.DecodePixelHeight = 600;
bitmap.EndInit(); bitmap.EndInit();
var brush = new ImageBrush(bitmap) { Stretch = Stretch.UniformToFill }; var brush = new ImageBrush(bitmap)
{
Stretch = Stretch.UniformToFill
};
return brush; return brush;
} }
else else
@ -548,19 +571,19 @@ namespace Flow.Launcher.ViewModel
{ {
Title = "WebSearch", Title = "WebSearch",
SubTitle = "Search the web with different search engine support", SubTitle = "Search the web with different search engine support",
IcoPath =Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png") IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png")
}, },
new Result new Result
{ {
Title = "Program", Title = "Program",
SubTitle = "Launch programs as admin or a different user", SubTitle = "Launch programs as admin or a different user",
IcoPath =Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png") IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png")
}, },
new Result new Result
{ {
Title = "ProcessKiller", Title = "ProcessKiller",
SubTitle = "Terminate unwanted processes", SubTitle = "Terminate unwanted processes",
IcoPath =Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png") IcoPath = Path.Combine(Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png")
} }
}; };
var vm = new ResultsViewModel(Settings); var vm = new ResultsViewModel(Settings);
@ -574,8 +597,8 @@ namespace Flow.Launcher.ViewModel
get get
{ {
if (Fonts.SystemFontFamilies.Count(o => if (Fonts.SystemFontFamilies.Count(o =>
o.FamilyNames.Values != null && o.FamilyNames.Values != null &&
o.FamilyNames.Values.Contains(Settings.QueryBoxFont)) > 0) o.FamilyNames.Values.Contains(Settings.QueryBoxFont)) > 0)
{ {
var font = new FontFamily(Settings.QueryBoxFont); var font = new FontFamily(Settings.QueryBoxFont);
return font; return font;
@ -602,7 +625,7 @@ namespace Flow.Launcher.ViewModel
Settings.QueryBoxFontStyle, Settings.QueryBoxFontStyle,
Settings.QueryBoxFontWeight, Settings.QueryBoxFontWeight,
Settings.QueryBoxFontStretch Settings.QueryBoxFontStretch
)); ));
return typeface; return typeface;
} }
set set
@ -619,8 +642,8 @@ namespace Flow.Launcher.ViewModel
get get
{ {
if (Fonts.SystemFontFamilies.Count(o => if (Fonts.SystemFontFamilies.Count(o =>
o.FamilyNames.Values != null && o.FamilyNames.Values != null &&
o.FamilyNames.Values.Contains(Settings.ResultFont)) > 0) o.FamilyNames.Values.Contains(Settings.ResultFont)) > 0)
{ {
var font = new FontFamily(Settings.ResultFont); var font = new FontFamily(Settings.ResultFont);
return font; return font;
@ -647,7 +670,7 @@ namespace Flow.Launcher.ViewModel
Settings.ResultFontStyle, Settings.ResultFontStyle,
Settings.ResultFontWeight, Settings.ResultFontWeight,
Settings.ResultFontStretch Settings.ResultFontStretch
)); ));
return typeface; return typeface;
} }
set set
@ -669,6 +692,65 @@ namespace Flow.Launcher.ViewModel
#endregion #endregion
#region shortcut
public CustomShortcutModel? SelectedCustomShortcut { get; set; }
public void DeleteSelectedCustomShortcut()
{
var item = SelectedCustomShortcut;
if (item == null)
{
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
return;
}
string deleteWarning = string.Format(
InternationalizationManager.Instance.GetTranslation("deleteCustomShortcutWarning"),
item?.Key, item?.Value);
if (MessageBox.Show(deleteWarning, InternationalizationManager.Instance.GetTranslation("delete"),
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
Settings.CustomShortcuts.Remove(item);
}
}
public bool EditSelectedCustomShortcut()
{
var item = SelectedCustomShortcut;
if (item == null)
{
MessageBox.Show(InternationalizationManager.Instance.GetTranslation("pleaseSelectAnItem"));
return false;
}
var shortcutSettingWindow = new CustomShortcutSetting(item.Key, item.Value, this);
if (shortcutSettingWindow.ShowDialog() == true)
{
item.Key = shortcutSettingWindow.Key;
item.Value = shortcutSettingWindow.Value;
return true;
}
return false;
}
public void AddCustomShortcut()
{
var shortcutSettingWindow = new CustomShortcutSetting(this);
if (shortcutSettingWindow.ShowDialog() == true)
{
var shortcut = new CustomShortcutModel(shortcutSettingWindow.Key, shortcutSettingWindow.Value);
Settings.CustomShortcuts.Add(shortcut);
}
}
public bool ShortcutExists(string key)
{
return Settings.CustomShortcuts.Any(x => x.Key == key) || Settings.BuiltinShortcuts.Any(x => x.Key == key);
}
#endregion
#region about #region about
public string Website => Constant.Website; public string Website => Constant.Website;
@ -676,7 +758,20 @@ namespace Flow.Launcher.ViewModel
public string Documentation => Constant.Documentation; public string Documentation => Constant.Documentation;
public string Docs => Constant.Docs; public string Docs => Constant.Docs;
public string Github => Constant.GitHub; public string Github => Constant.GitHub;
public static string Version => Constant.Version; public string Version
{
get
{
if (Constant.Version == "1.0.0")
{
return Constant.Dev;
}
else
{
return Constant.Version;
}
}
}
public string ActivatedTimes => string.Format(_translater.GetTranslation("about_activate_times"), Settings.ActivateTimes); public string ActivatedTimes => string.Format(_translater.GetTranslation("about_activate_times"), Settings.ActivateTimes);
public string CheckLogFolder public string CheckLogFolder

View file

@ -1,4 +1,4 @@
using Flow.Launcher.Plugin.BrowserBookmark.Models; using Flow.Launcher.Plugin.BrowserBookmark.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.SQLite; using System.Data.SQLite;
@ -130,4 +130,4 @@ namespace Flow.Launcher.Plugin.BrowserBookmark
} }
} }
} }
} }

View file

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>net6.0-windows</TargetFramework> <TargetFramework>net6.0-windows</TargetFramework>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<ProjectGuid>{9B130CC5-14FB-41FF-B310-0A95B6894C37}</ProjectGuid> <ProjectGuid>{9B130CC5-14FB-41FF-B310-0A95B6894C37}</ProjectGuid>
<AppDesignerFolder>Properties</AppDesignerFolder> <AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Flow.Launcher.Plugin.BrowserBookmark</RootNamespace> <RootNamespace>Flow.Launcher.Plugin.BrowserBookmark</RootNamespace>
@ -23,7 +23,7 @@
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit> <Prefer32Bit>false</Prefer32Bit>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType> <DebugType>pdbonly</DebugType>
<Optimize>true</Optimize> <Optimize>true</Optimize>
@ -39,15 +39,6 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="x64\SQLite.Interop.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="x86\SQLite.Interop.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" /> <ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
@ -64,8 +55,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Data.SQLite" Version="1.0.114.4" /> <PackageReference Include="System.Data.SQLite" Version="1.0.116" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.114.3" /> <PackageReference Include="System.Data.SQLite.Core" Version="1.0.116" />
<PackageReference Include="UnidecodeSharp" Version="1.0.0" /> <PackageReference Include="UnidecodeSharp" Version="1.0.0" />
</ItemGroup> </ItemGroup>
@ -73,4 +64,19 @@
<Compile Remove="Bookmark.cs" /> <Compile Remove="Bookmark.cs" />
</ItemGroup> </ItemGroup>
<Target Name="CopyDLLs" AfterTargets="Build">
<Message Text="Executing CopyDLLs task" Importance="High" />
<Copy
SourceFiles="$(TargetDir)\runtimes\win-x64\native\SQLite.Interop.dll"
DestinationFolder="$(TargetDir)\x64" />
<Copy
SourceFiles="$(TargetDir)\runtimes\win-x86\native\SQLite.Interop.dll"
DestinationFolder="$(TargetDir)\x86" />
</Target>
<Target Name="DeleteRuntimesFolder" AfterTargets="CopyDLLs">
<Message Text="Deleting runtimes folder" Importance="High" />
<RemoveDir Directories="$(TargetDir)\runtimes" />
</Target>
</Project> </Project>

View file

@ -6,12 +6,13 @@
xmlns:local="clr-namespace:Flow.Launcher.Plugin.Explorer.Views" xmlns:local="clr-namespace:Flow.Launcher.Plugin.Explorer.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="{DynamicResource plugin_explorer_manageactionkeywords_header}" Title="{DynamicResource plugin_explorer_manageactionkeywords_header}"
Width="400" Width="Auto"
SizeToContent="Height" Height="255"
Background="{DynamicResource PopuBGColor}" Background="{DynamicResource PopuBGColor}"
DataContext="{Binding RelativeSource={RelativeSource Self}}" DataContext="{Binding RelativeSource={RelativeSource Self}}"
Foreground="{DynamicResource PopupTextColor}" Foreground="{DynamicResource PopupTextColor}"
ResizeMode="NoResize" ResizeMode="NoResize"
SizeToContent="Width"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
<WindowChrome.WindowChrome> <WindowChrome.WindowChrome>
@ -68,7 +69,7 @@
<StackPanel Margin="0,10,0,0" Orientation="Horizontal"> <StackPanel Margin="0,10,0,0" Orientation="Horizontal">
<TextBlock <TextBlock
Width="150" MinWidth="150"
Margin="0,10,15,10" Margin="0,10,15,10"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
@ -78,18 +79,19 @@
Name="TxtCurrentActionKeyword" Name="TxtCurrentActionKeyword"
Grid.Row="0" Grid.Row="0"
Grid.Column="1" Grid.Column="1"
Width="105" Width="135"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
PreviewKeyDown="TxtCurrentActionKeyword_OnKeyDown" PreviewKeyDown="TxtCurrentActionKeyword_OnKeyDown"
Text="{Binding ActionKeyword}" /> Text="{Binding ActionKeyword}" />
</StackPanel> </StackPanel>
<StackPanel Margin="0,10,0,15" Orientation="Horizontal"> <StackPanel Margin="0,10,0,15" Orientation="Horizontal">
<TextBlock Margin="0 0 15 0 " <TextBlock
MinWidth="150"
Margin="0,0,18,0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
FontSize="14" FontSize="14"
Width="150"
Text="{DynamicResource plugin_explorer_actionkeyword_enabled}" /> Text="{DynamicResource plugin_explorer_actionkeyword_enabled}" />
<CheckBox <CheckBox
Name="ChkActionKeywordEnabled" Name="ChkActionKeywordEnabled"

View file

@ -5,12 +5,13 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="{DynamicResource flowlauncher_plugin_program_directory}" Title="{DynamicResource flowlauncher_plugin_program_directory}"
Width="400" Width="Auto"
Height="276"
Background="{DynamicResource PopuBGColor}" Background="{DynamicResource PopuBGColor}"
Foreground="{DynamicResource PopupTextColor}" Foreground="{DynamicResource PopupTextColor}"
SizeToContent="Height" ResizeMode="NoResize"
WindowStartupLocation="CenterScreen" SizeToContent="Width"
mc:Ignorable="d"> WindowStartupLocation="CenterScreen">
<WindowChrome.WindowChrome> <WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" /> <WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome> </WindowChrome.WindowChrome>
@ -19,7 +20,6 @@
<RowDefinition /> <RowDefinition />
<RowDefinition Height="80" /> <RowDefinition Height="80" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel Grid.Row="0"> <StackPanel Grid.Row="0">
<StackPanel> <StackPanel>
<Grid> <Grid>
@ -53,33 +53,80 @@
</Button> </Button>
</Grid> </Grid>
</StackPanel> </StackPanel>
<StackPanel Margin="26,12,26,0"> <StackPanel Margin="26,0,26,0">
<StackPanel Margin="0,0,0,12"> <StackPanel Grid.Row="0" Margin="0,0,0,12">
<TextBlock <TextBlock
Grid.Column="0" Grid.Column="0"
Margin="0,0,0,0" Margin="0,0,0,0"
FontSize="20" FontSize="20"
FontWeight="SemiBold" FontWeight="SemiBold"
Text="{DynamicResource flowlauncher_plugin_program_directory}" Text="{DynamicResource flowlauncher_plugin_program_edit_program_source_title}"
TextAlignment="Left" /> TextAlignment="Left" />
</StackPanel> </StackPanel>
<StackPanel Margin="0,0,0,10" Orientation="Horizontal"> <StackPanel>
<TextBox <TextBlock
Name="Directory" FontSize="14"
Width="268" Text="{DynamicResource flowlauncher_plugin_program_edit_program_source_tips}"
Margin="0,7" TextAlignment="Left"
VerticalAlignment="Center" /> TextWrapping="WrapWithOverflow" />
<Button </StackPanel>
Width="70"
Margin="10" <StackPanel Margin="0,10,10,0" Orientation="Horizontal">
HorizontalAlignment="Right" <Grid>
Click="BrowseButton_Click" <Grid.RowDefinitions>
Content="{DynamicResource flowlauncher_plugin_program_browse}" /> <RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource flowlauncher_plugin_program_directory}" />
<DockPanel
Grid.Row="0"
Grid.Column="1"
LastChildFill="True">
<Button
MinWidth="70"
HorizontalAlignment="Stretch"
Click="BrowseButton_Click"
Content="{DynamicResource flowlauncher_plugin_program_browse}"
DockPanel.Dock="Right" />
<TextBox
Name="Directory"
MinWidth="300"
Margin="10"
HorizontalAlignment="Stretch"
VerticalAlignment="Center" />
</DockPanel>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="10"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource flowlauncher_plugin_program_enabled}" />
<CheckBox
x:Name="Chkbox"
Grid.Row="1"
Grid.Column="1"
Margin="10,0"
VerticalAlignment="Center" />
</Grid>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<Border <Border
Grid.Row="1" Grid.Row="1"
Margin="0,14,0,0"
Background="{DynamicResource PopupButtonAreaBGColor}" Background="{DynamicResource PopupButtonAreaBGColor}"
BorderBrush="{DynamicResource PopupButtonAreaBorderColor}" BorderBrush="{DynamicResource PopupButtonAreaBorderColor}"
BorderThickness="0,1,0,0"> BorderThickness="0,1,0,0">
@ -87,19 +134,17 @@
<Button <Button
x:Name="btnCancel" x:Name="btnCancel"
MinWidth="140" MinWidth="140"
Margin="0,0,5,0" Margin="10,0,5,0"
Click="BtnCancel_OnClick" Click="BtnCancel_OnClick"
Content="{DynamicResource cancel}" /> Content="{DynamicResource cancel}" />
<Button <Button
x:Name="btnAdd"
MinWidth="140" MinWidth="140"
Margin="5,0,0,0" Margin="5,0,10,0"
HorizontalAlignment="Right" Click="BtnAdd_OnClick"
Click="ButtonAdd_OnClick"
Content="{DynamicResource flowlauncher_plugin_program_update}" Content="{DynamicResource flowlauncher_plugin_program_update}"
Style="{DynamicResource AccentButtonStyle}" /> Style="{DynamicResource AccentButtonStyle}" />
</StackPanel> </StackPanel>
</Border> </Border>
</Grid> </Grid>
</Window>
</Window>

View file

@ -9,11 +9,12 @@ namespace Flow.Launcher.Plugin.Program
/// <summary> /// <summary>
/// Interaction logic for AddProgramSource.xaml /// Interaction logic for AddProgramSource.xaml
/// </summary> /// </summary>
public partial class AddProgramSource public partial class AddProgramSource : Window
{ {
private PluginInitContext _context; private PluginInitContext _context;
private Settings.ProgramSource _editing; private ProgramSource _editing;
private Settings _settings; private Settings _settings;
private bool update;
public AddProgramSource(PluginInitContext context, Settings settings) public AddProgramSource(PluginInitContext context, Settings settings)
{ {
@ -21,14 +22,19 @@ namespace Flow.Launcher.Plugin.Program
_context = context; _context = context;
_settings = settings; _settings = settings;
Directory.Focus(); Directory.Focus();
Chkbox.IsChecked = true;
update = false;
btnAdd.Content = _context.API.GetTranslation("flowlauncher_plugin_program_add");
} }
public AddProgramSource(Settings.ProgramSource edit, Settings settings) public AddProgramSource(PluginInitContext context, Settings settings, ProgramSource source)
{ {
_editing = edit;
_settings = settings;
InitializeComponent(); InitializeComponent();
_context = context;
_editing = source;
_settings = settings;
update = true;
Chkbox.IsChecked = _editing.Enabled;
Directory.Text = _editing.Location; Directory.Text = _editing.Location;
} }
@ -47,34 +53,54 @@ namespace Flow.Launcher.Plugin.Program
Close(); Close();
} }
private void ButtonAdd_OnClick(object sender, RoutedEventArgs e) private void BtnAdd_OnClick(object sender, RoutedEventArgs e)
{ {
string s = Directory.Text; string path = Directory.Text;
if (!System.IO.Directory.Exists(s)) bool modified = false;
if (!System.IO.Directory.Exists(path))
{ {
System.Windows.MessageBox.Show(_context.API.GetTranslation("flowlauncher_plugin_program_invalid_path")); System.Windows.MessageBox.Show(_context.API.GetTranslation("flowlauncher_plugin_program_invalid_path"));
return; return;
} }
if (_editing == null) if (!update)
{ {
if (!ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == Directory.Text)) if (!ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier.Equals(path, System.StringComparison.OrdinalIgnoreCase)))
{ {
var source = new ProgramSource var source = new ProgramSource(path);
{ modified = true;
Location = Directory.Text,
UniqueIdentifier = Directory.Text
};
_settings.ProgramSources.Insert(0, source); _settings.ProgramSources.Insert(0, source);
ProgramSetting.ProgramSettingDisplayList.Add(source); ProgramSetting.ProgramSettingDisplayList.Add(source);
} }
else
{
System.Windows.MessageBox.Show(_context.API.GetTranslation("flowlauncher_plugin_program_duplicate_program_source"));
return;
}
} }
else else
{ {
_editing.Location = Directory.Text; // Separate checks to avoid changing UniqueIdentifier of UWP
if (!_editing.Location.Equals(path, System.StringComparison.OrdinalIgnoreCase))
{
if (ProgramSetting.ProgramSettingDisplayList
.Any(x => x.UniqueIdentifier.Equals(path, System.StringComparison.OrdinalIgnoreCase)))
{
// Check if the new location is used
// No need to check win32 or uwp, just override them
System.Windows.MessageBox.Show(_context.API.GetTranslation("flowlauncher_plugin_program_duplicate_program_source"));
return;
}
modified = true;
_editing.Location = path; // Changes UniqueIdentifier internally
}
if (_editing.Enabled != Chkbox.IsChecked)
{
modified = true;
_editing.Enabled = Chkbox.IsChecked ?? true;
}
} }
DialogResult = true; DialogResult = modified;
Close(); Close();
} }
} }

View file

@ -1,58 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.Program.Programs;
namespace Flow.Launcher.Plugin.Program
{
//internal static class FileChangeWatcher
//{
// private static readonly List<string> WatchedPath = new List<string>();
// // todo remove previous watcher events
// public static void AddAll(List<UnregisteredPrograms> sources, string[] suffixes)
// {
// foreach (var s in sources)
// {
// if (Directory.Exists(s.Location))
// {
// AddWatch(s.Location, suffixes);
// }
// }
// }
// public static void AddWatch(string path, string[] programSuffixes, bool includingSubDirectory = true)
// {
// if (WatchedPath.Contains(path)) return;
// if (!Directory.Exists(path))
// {
// Log.Warn($"|FileChangeWatcher|{path} doesn't exist");
// return;
// }
// WatchedPath.Add(path);
// foreach (string fileType in programSuffixes)
// {
// FileSystemWatcher watcher = new FileSystemWatcher
// {
// Path = path,
// IncludeSubdirectories = includingSubDirectory,
// Filter = $"*.{fileType}",
// EnableRaisingEvents = true
// };
// watcher.Changed += FileChanged;
// watcher.Created += FileChanged;
// watcher.Deleted += FileChanged;
// watcher.Renamed += FileChanged;
// }
// }
// private static void FileChanged(object source, FileSystemEventArgs e)
// {
// Task.Run(() =>
// {
// Main.IndexPrograms();
// });
// }
//}
}

View file

@ -10,16 +10,21 @@
<system:String x:Key="flowlauncher_plugin_program_add">Add</system:String> <system:String x:Key="flowlauncher_plugin_program_add">Add</system:String>
<system:String x:Key="flowlauncher_plugin_program_name">Name</system:String> <system:String x:Key="flowlauncher_plugin_program_name">Name</system:String>
<system:String x:Key="flowlauncher_plugin_program_enable">Enable</system:String> <system:String x:Key="flowlauncher_plugin_program_enable">Enable</system:String>
<system:String x:Key="flowlauncher_plugin_program_enabled">Enabled</system:String>
<system:String x:Key="flowlauncher_plugin_program_disable">Disable</system:String> <system:String x:Key="flowlauncher_plugin_program_disable">Disable</system:String>
<system:String x:Key="flowlauncher_plugin_program_location">Location</system:String> <system:String x:Key="flowlauncher_plugin_program_location">Location</system:String>
<system:String x:Key="flowlauncher_plugin_program_all_programs">All Programs</system:String> <system:String x:Key="flowlauncher_plugin_program_all_programs">All Programs</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes">File Type</system:String> <system:String x:Key="flowlauncher_plugin_program_suffixes">File Type</system:String>
<system:String x:Key="flowlauncher_plugin_program_reindex">Reindex</system:String> <system:String x:Key="flowlauncher_plugin_program_reindex">Reindex</system:String>
<system:String x:Key="flowlauncher_plugin_program_indexing">Indexing</system:String> <system:String x:Key="flowlauncher_plugin_program_indexing">Indexing</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start">Index Start Menu</system:String> <system:String x:Key="flowlauncher_plugin_program_index_source">Index Sources</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_option">Options</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start">Start Menu</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_start_tooltip">When enabled, Flow will load programs from the start menu</system:String> <system:String x:Key="flowlauncher_plugin_program_index_start_tooltip">When enabled, Flow will load programs from the start menu</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_registry">Index Registry</system:String> <system:String x:Key="flowlauncher_plugin_program_index_registry">Registry</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_registry_tooltip">When enabled, Flow will load programs from the registry</system:String> <system:String x:Key="flowlauncher_plugin_program_index_registry_tooltip">When enabled, Flow will load programs from the registry</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_PATH">PATH</system:String>
<system:String x:Key="flowlauncher_plugin_program_index_PATH_tooltip">When enabled, Flow will load programs from the PATH environment variable</system:String>
<system:String x:Key="flowlauncher_plugin_program_enable_hidelnkpath">Hide app path</system:String> <system:String x:Key="flowlauncher_plugin_program_enable_hidelnkpath">Hide app path</system:String>
<system:String x:Key="flowlauncher_plugin_program_enable_hidelnkpath_tooltip">For executable files such as UWP or lnk, hide the file path from being visible</system:String> <system:String x:Key="flowlauncher_plugin_program_enable_hidelnkpath_tooltip">For executable files such as UWP or lnk, hide the file path from being visible</system:String>
<system:String x:Key="flowlauncher_plugin_program_enable_description">Search in Program Description</system:String> <system:String x:Key="flowlauncher_plugin_program_enable_description">Search in Program Description</system:String>
@ -34,8 +39,12 @@
<system:String x:Key="flowlauncher_plugin_program_pls_select_program_source">Please select a program source</system:String> <system:String x:Key="flowlauncher_plugin_program_pls_select_program_source">Please select a program source</system:String>
<system:String x:Key="flowlauncher_plugin_program_delete_program_source">Are you sure you want to delete the selected program sources?</system:String> <system:String x:Key="flowlauncher_plugin_program_delete_program_source">Are you sure you want to delete the selected program sources?</system:String>
<system:String x:Key="flowlauncher_plugin_program_duplicate_program_source">Another program source with the same location alreaday exists.</system:String>
<system:String x:Key="flowlauncher_plugin_program_update">OK</system:String> <system:String x:Key="flowlauncher_plugin_program_edit_program_source_title">Program Source</system:String>
<system:String x:Key="flowlauncher_plugin_program_edit_program_source_tips">Edit directory and status of this program source.</system:String>
<system:String x:Key="flowlauncher_plugin_program_update">Update</system:String>
<system:String x:Key="flowlauncher_plugin_program_only_index_tip">Program Plugin will only index files with selected suffixes and .url files with selected protocols.</system:String> <system:String x:Key="flowlauncher_plugin_program_only_index_tip">Program Plugin will only index files with selected suffixes and .url files with selected protocols.</system:String>
<system:String x:Key="flowlauncher_plugin_program_update_file_suffixes">Successfully updated file suffixes</system:String> <system:String x:Key="flowlauncher_plugin_program_update_file_suffixes">Successfully updated file suffixes</system:String>
<system:String x:Key="flowlauncher_plugin_program_suffixes_cannot_empty">File suffixes can't be empty</system:String> <system:String x:Key="flowlauncher_plugin_program_suffixes_cannot_empty">File suffixes can't be empty</system:String>
@ -52,7 +61,7 @@
Insert file suffixes you want to index. Suffixes should be separated by ';'. (ex>bat;py) Insert file suffixes you want to index. Suffixes should be separated by ';'. (ex>bat;py)
</system:String> </system:String>
<system:String x:Key="flowlauncher_plugin_program_protocol_tooltip"> <system:String x:Key="flowlauncher_plugin_program_protocol_tooltip">
Insert protocols of .url files you want to index. Protocols should be separated by ';'. (ex>ftp;netflix) Insert protocols of .url files you want to index. Protocols should be separated by ';', and should end with "://". (ex>ftp://;mailto://)
</system:String> </system:String>
<system:String x:Key="flowlauncher_plugin_program_run_as_different_user">Run As Different User</system:String> <system:String x:Key="flowlauncher_plugin_program_run_as_different_user">Run As Different User</system:String>

View file

@ -1,13 +1,8 @@
using NLog; using NLog;
using NLog.Config;
using NLog.Targets;
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security; using System.Security;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.UserSettings;
namespace Flow.Launcher.Plugin.Program.Logger namespace Flow.Launcher.Plugin.Program.Logger
{ {

View file

@ -1,7 +1,5 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -11,9 +9,8 @@ using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin.Program.Programs; using Flow.Launcher.Plugin.Program.Programs;
using Flow.Launcher.Plugin.Program.Views; using Flow.Launcher.Plugin.Program.Views;
using Flow.Launcher.Plugin.Program.Views.Models;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
namespace Flow.Launcher.Plugin.Program namespace Flow.Launcher.Plugin.Program
@ -82,13 +79,9 @@ namespace Flow.Launcher.Plugin.Program
Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", () => Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", () =>
{ {
_win32Storage = new BinaryStorage<Win32[]>("Win32"); _win32Storage = new BinaryStorage<Win32[]>("Win32");
_win32s = _win32Storage.TryLoad(new Win32[] _win32s = _win32Storage.TryLoad(Array.Empty<Win32>());
{
});
_uwpStorage = new BinaryStorage<UWP.Application[]>("UWP"); _uwpStorage = new BinaryStorage<UWP.Application[]>("UWP");
_uwps = _uwpStorage.TryLoad(new UWP.Application[] _uwps = _uwpStorage.TryLoad(Array.Empty<UWP.Application>());
{
});
}); });
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>"); Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>");
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>"); Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>");
@ -102,14 +95,14 @@ namespace Flow.Launcher.Plugin.Program
var b = Task.Run(() => var b = Task.Run(() =>
{ {
Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexUwpPrograms); Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|UWPPRogram index cost", IndexUwpPrograms);
}); });
if (cacheEmpty) if (cacheEmpty)
await Task.WhenAll(a, b); await Task.WhenAll(a, b);
Win32.WatchProgramUpdate(_settings); Win32.WatchProgramUpdate(_settings);
UWP.WatchPackageChange(); _ = UWP.WatchPackageChange();
} }
public static void IndexWin32Programs() public static void IndexWin32Programs()
@ -123,18 +116,23 @@ namespace Flow.Launcher.Plugin.Program
{ {
var windows10 = new Version(10, 0); var windows10 = new Version(10, 0);
var support = Environment.OSVersion.Version.Major >= windows10.Major; var support = Environment.OSVersion.Version.Major >= windows10.Major;
var applications = support ? UWP.All() : new UWP.Application[] var applications = support ? UWP.All() : Array.Empty<UWP.Application>();
{
};
_uwps = applications; _uwps = applications;
ResetCache(); ResetCache();
} }
public static async Task IndexProgramsAsync() public static async Task IndexProgramsAsync()
{ {
var t1 = Task.Run(IndexWin32Programs); var a = Task.Run(() =>
var t2 = Task.Run(IndexUwpPrograms); {
await Task.WhenAll(t1, t2).ConfigureAwait(false); Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs);
});
var b = Task.Run(() =>
{
Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|UWPProgram index cost", IndexUwpPrograms);
});
await Task.WhenAll(a, b).ConfigureAwait(false);
_settings.LastIndexTime = DateTime.Today; _settings.LastIndexTime = DateTime.Today;
} }
@ -190,29 +188,33 @@ namespace Flow.Launcher.Plugin.Program
return menuOptions; return menuOptions;
} }
private void DisableProgram(IProgram programToDelete) private static void DisableProgram(IProgram programToDelete)
{ {
if (_settings.DisabledProgramSources.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) if (_settings.DisabledProgramSources.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
return; return;
if (_uwps.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) if (_uwps.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
_uwps.FirstOrDefault(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier) {
.Enabled = false; var program = _uwps.First(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier);
program.Enabled = false;
if (_win32s.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) _settings.DisabledProgramSources.Add(new ProgramSource(program));
_win32s.FirstOrDefault(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier) _ = Task.Run(() =>
.Enabled = false; {
IndexUwpPrograms();
_settings.DisabledProgramSources _settings.LastIndexTime = DateTime.Today;
.Add( });
new Settings.DisabledProgramSource }
{ else if (_win32s.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier))
Name = programToDelete.Name, {
Location = programToDelete.Location, var program = _win32s.First(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier);
UniqueIdentifier = programToDelete.UniqueIdentifier, program.Enabled = false;
Enabled = false _settings.DisabledProgramSources.Add(new ProgramSource(program));
} _ = Task.Run(() =>
); {
IndexWin32Programs();
_settings.LastIndexTime = DateTime.Today;
});
}
} }
public static void StartProcess(Func<ProcessStartInfo, Process> runProcess, ProcessStartInfo info) public static void StartProcess(Func<ProcessStartInfo, Process> runProcess, ProcessStartInfo info)
@ -233,6 +235,7 @@ namespace Flow.Launcher.Plugin.Program
{ {
await IndexProgramsAsync(); await IndexProgramsAsync();
} }
public void Dispose() public void Dispose()
{ {
Win32.Dispose(); Win32.Dispose();

View file

@ -8,8 +8,8 @@
Title="{DynamicResource flowlauncher_plugin_program_suffixes}" Title="{DynamicResource flowlauncher_plugin_program_suffixes}"
Width="600" Width="600"
Background="{DynamicResource PopuBGColor}" Background="{DynamicResource PopuBGColor}"
Foreground="{DynamicResource PopupTextColor}"
DataContext="{Binding RelativeSource={RelativeSource Self}}" DataContext="{Binding RelativeSource={RelativeSource Self}}"
Foreground="{DynamicResource PopupTextColor}"
ResizeMode="NoResize" ResizeMode="NoResize"
SizeToContent="Height" SizeToContent="Height"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
@ -163,17 +163,32 @@
FontSize="16" FontSize="16"
FontWeight="SemiBold" FontWeight="SemiBold"
Text="{DynamicResource flowlauncher_plugin_program_suffixes_excutable_types}" /> Text="{DynamicResource flowlauncher_plugin_program_suffixes_excutable_types}" />
<CheckBox Name="apprefMS" Margin="10,0,0,0" IsChecked="{Binding SuffixesStatus[appref-ms]}">appref-ms</CheckBox> <CheckBox
<CheckBox Name="exe" Margin="10,0,0,0" IsChecked="{Binding SuffixesStatus[exe]}">exe</CheckBox> Name="apprefMS"
<CheckBox Name="lnk" Margin="10,0,0,0" IsChecked="{Binding SuffixesStatus[lnk]}">lnk</CheckBox> Margin="10,0,0,0"
IsChecked="{Binding SuffixesStatus[appref-ms]}">
appref-ms
</CheckBox>
<CheckBox
Name="exe"
Margin="10,0,0,0"
IsChecked="{Binding SuffixesStatus[exe]}">
exe
</CheckBox>
<CheckBox
Name="lnk"
Margin="10,0,0,0"
IsChecked="{Binding SuffixesStatus[lnk]}">
lnk
</CheckBox>
<CheckBox <CheckBox
Name="CustomFiles" Name="CustomFiles"
Margin="10,0,0,0" Margin="10,0,0,0"
IsChecked="{Binding UseCustomSuffixes}" Content="{DynamicResource flowlauncher_plugin_program_suffixes_custom_file_types}"
Content="{DynamicResource flowlauncher_plugin_program_suffixes_custom_file_types}" /> IsChecked="{Binding UseCustomSuffixes}" />
<TextBox <TextBox
x:Name="tbSuffixes" x:Name="tbSuffixes"
Margin="10,4,0,6" Margin="10,4,0,10"
Style="{StaticResource CustomFileTypeTextBox}" /> Style="{StaticResource CustomFileTypeTextBox}" />
</StackPanel> </StackPanel>
@ -189,17 +204,29 @@
FontSize="16" FontSize="16"
FontWeight="SemiBold" FontWeight="SemiBold"
Text="{DynamicResource flowlauncher_plugin_program_suffixes_URL_types}" /> Text="{DynamicResource flowlauncher_plugin_program_suffixes_URL_types}" />
<CheckBox Name="steam" Margin="10,0,0,0" IsChecked="{Binding ProtocolsStatus[steam]}" Content="{DynamicResource flowlauncher_plugin_program_suffixes_URL_steam}"></CheckBox> <CheckBox
<CheckBox Name="epic" Margin="10,0,0,0" IsChecked="{Binding ProtocolsStatus[epic]}" Content="{DynamicResource flowlauncher_plugin_program_suffixes_URL_epic}"></CheckBox> Name="steam"
<CheckBox Name="http" Margin="10,0,0,0" IsChecked="{Binding ProtocolsStatus[http]}" Content="{DynamicResource flowlauncher_plugin_program_suffixes_URL_http}"></CheckBox> Margin="10,0,0,0"
Content="{DynamicResource flowlauncher_plugin_program_suffixes_URL_steam}"
IsChecked="{Binding ProtocolsStatus[steam]}" />
<CheckBox
Name="epic"
Margin="10,0,0,0"
Content="{DynamicResource flowlauncher_plugin_program_suffixes_URL_epic}"
IsChecked="{Binding ProtocolsStatus[epic]}" />
<CheckBox
Name="http"
Margin="10,0,0,0"
Content="{DynamicResource flowlauncher_plugin_program_suffixes_URL_http}"
IsChecked="{Binding ProtocolsStatus[http]}" />
<CheckBox <CheckBox
Name="CustomProtocol" Name="CustomProtocol"
Margin="10,0,0,0" Margin="10,0,0,0"
IsChecked="{Binding UseCustomProtocols}" Content="{DynamicResource flowlauncher_plugin_program_suffixes_custom_urls}"
Content="{DynamicResource flowlauncher_plugin_program_suffixes_custom_urls}" /> IsChecked="{Binding UseCustomProtocols}" />
<TextBox <TextBox
x:Name="tbProtocols" x:Name="tbProtocols"
Margin="10,4,0,6" Margin="10,4,0,0"
Style="{StaticResource CustomURLTypeTextBox}" /> Style="{StaticResource CustomURLTypeTextBox}" />
</StackPanel> </StackPanel>
</Border> </Border>

View file

@ -1,8 +1,6 @@
using System; using System;
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
namespace Flow.Launcher.Plugin.Program.Programs namespace Flow.Launcher.Plugin.Program.Programs
{ {

View file

@ -1,20 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes; using System.Runtime.InteropServices.ComTypes;
using Windows.Storage;
namespace Flow.Launcher.Plugin.Program.Programs namespace Flow.Launcher.Plugin.Program.Programs
{ {
public class AppxPackageHelper public class AppxPackageHelper
{ {
// This function returns a list of attributes of applications // This function returns a list of attributes of applications
public List<IAppxManifestApplication> getAppsFromManifest(IStream stream) public static List<IAppxManifestApplication> GetAppsFromManifest(IStream stream)
{ {
IAppxFactory appxFactory = (IAppxFactory)new AppxFactory();
List<IAppxManifestApplication> apps = new List<IAppxManifestApplication>(); List<IAppxManifestApplication> apps = new List<IAppxManifestApplication>();
var appxFactory = new AppxFactory(); var reader = appxFactory.CreateManifestReader(stream);
var reader = ((IAppxFactory)appxFactory).CreateManifestReader(stream);
var manifestApps = reader.GetApplications(); var manifestApps = reader.GetApplications();
while (manifestApps.GetHasCurrent()) while (manifestApps.GetHasCurrent())
{ {

View file

@ -1,11 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Text; using System.Text;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.IO;
using Accessibility; using Accessibility;
using System.Runtime.InteropServices.ComTypes; using System.Runtime.InteropServices.ComTypes;
using System.Security.Policy;
namespace Flow.Launcher.Plugin.Program.Programs namespace Flow.Launcher.Plugin.Program.Programs
{ {
@ -100,8 +97,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
} }
// To initialize the app description // To initialize the app description
public String description = String.Empty; public string description = string.Empty;
public string arguments = string.Empty;
// Retrieve the target path using Shell Link // Retrieve the target path using Shell Link
public string retrieveTargetPath(string path) public string retrieveTargetPath(string path)
@ -125,13 +122,16 @@ namespace Flow.Launcher.Plugin.Program.Programs
buffer = new StringBuilder(MAX_PATH); buffer = new StringBuilder(MAX_PATH);
((IShellLinkW)link).GetDescription(buffer, MAX_PATH); ((IShellLinkW)link).GetDescription(buffer, MAX_PATH);
description = buffer.ToString(); description = buffer.ToString();
buffer.Clear();
((IShellLinkW)link).GetArguments(buffer, MAX_PATH);
arguments = buffer.ToString();
} }
// To release unmanaged memory // To release unmanaged memory
Marshal.ReleaseComObject(link); Marshal.ReleaseComObject(link);
return target; return target;
} }
} }
} }

View file

@ -18,7 +18,6 @@ using Flow.Launcher.Plugin.Program.Logger;
using Rect = System.Windows.Rect; using Rect = System.Windows.Rect;
using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using System.Runtime.Versioning;
using System.Threading.Channels; using System.Threading.Channels;
namespace Flow.Launcher.Plugin.Program.Programs namespace Flow.Launcher.Plugin.Program.Programs
@ -42,39 +41,27 @@ namespace Flow.Launcher.Plugin.Program.Programs
FullName = package.Id.FullName; FullName = package.Id.FullName;
FamilyName = package.Id.FamilyName; FamilyName = package.Id.FamilyName;
InitializeAppInfo(); InitializeAppInfo();
Apps = Apps.Where(a =>
{
var valid =
!string.IsNullOrEmpty(a.UserModelId) &&
!string.IsNullOrEmpty(a.DisplayName);
return valid;
}).ToArray();
} }
private void InitializeAppInfo() private void InitializeAppInfo()
{ {
AppxPackageHelper _helper = new AppxPackageHelper();
var path = Path.Combine(Location, "AppxManifest.xml"); var path = Path.Combine(Location, "AppxManifest.xml");
var namespaces = XmlNamespaces(path); var namespaces = XmlNamespaces(path);
InitPackageVersion(namespaces); InitPackageVersion(namespaces);
const uint noAttribute = 0x80; const uint noAttribute = 0x80;
const Stgm exclusiveRead = Stgm.Read | Stgm.ShareExclusive; const Stgm nonExclusiveRead = Stgm.Read | Stgm.ShareDenyNone;
var hResult = SHCreateStreamOnFileEx(path, exclusiveRead, noAttribute, false, null, out IStream stream); var hResult = SHCreateStreamOnFileEx(path, nonExclusiveRead, noAttribute, false, null, out IStream stream);
if (hResult == Hresult.Ok) if (hResult == Hresult.Ok)
{ {
var apps = new List<Application>(); List<AppxPackageHelper.IAppxManifestApplication> _apps = AppxPackageHelper.GetAppsFromManifest(stream);
List<AppxPackageHelper.IAppxManifestApplication> _apps = _helper.getAppsFromManifest(stream); Apps = _apps.Select(x => new Application(x, this))
foreach (var _app in _apps) .Where(a => !string.IsNullOrEmpty(a.UserModelId)
{ && !string.IsNullOrEmpty(a.DisplayName))
var app = new Application(_app, this); .ToArray();
apps.Add(app);
}
Apps = apps.Where(a => a.AppListEntry != "none").ToArray();
} }
else else
{ {
@ -82,17 +69,17 @@ namespace Flow.Launcher.Plugin.Program.Programs
ProgramLogger.LogException($"|UWP|InitializeAppInfo|{path}" + ProgramLogger.LogException($"|UWP|InitializeAppInfo|{path}" +
"|Error caused while trying to get the details of the UWP program", e); "|Error caused while trying to get the details of the UWP program", e);
Apps = new List<Application>().ToArray(); Apps = Array.Empty<Application>();
} }
if (Marshal.ReleaseComObject(stream) > 0) if (stream != null && Marshal.ReleaseComObject(stream) > 0)
{ {
Log.Error("Flow.Launcher.Plugin.Program.Programs.UWP", "AppxManifest.xml was leaked"); Log.Error("Flow.Launcher.Plugin.Program.Programs.UWP", "AppxManifest.xml was leaked");
} }
} }
/// http://www.hanselman.com/blog/GetNamespacesFromAnXMLDocumentWithXPathDocumentAndLINQToXML.aspx /// http://www.hanselman.com/blog/GetNamespacesFromAnXMLDocumentWithXPathDocumentAndLINQToXML.aspx
private string[] XmlNamespaces(string path) private static string[] XmlNamespaces(string path)
{ {
XDocument z = XDocument.Load(path); XDocument z = XDocument.Load(path);
if (z.Root != null) if (z.Root != null)
@ -110,9 +97,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
ProgramLogger.LogException($"|UWP|XmlNamespaces|{path}" + ProgramLogger.LogException($"|UWP|XmlNamespaces|{path}" +
$"|Error occured while trying to get the XML from {path}", new ArgumentNullException()); $"|Error occured while trying to get the XML from {path}", new ArgumentNullException());
return new string[] return Array.Empty<string>();
{
};
} }
} }
@ -178,16 +163,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
var updatedListWithoutDisabledApps = applications var updatedListWithoutDisabledApps = applications
.Where(t1 => !Main._settings.DisabledProgramSources .Where(t1 => !Main._settings.DisabledProgramSources
.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)) .Any(x => x.UniqueIdentifier == t1.UniqueIdentifier));
.Select(x => x);
return updatedListWithoutDisabledApps.ToArray(); return updatedListWithoutDisabledApps.ToArray();
} }
else else
{ {
return new Application[] return Array.Empty<Application>();
{
};
} }
} }
@ -233,9 +215,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
} }
else else
{ {
return new Package[] return Array.Empty<Package>();
{
};
} }
} }
@ -297,8 +277,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
[Serializable] [Serializable]
public class Application : IProgram public class Application : IProgram
{ {
public string AppListEntry { get; set; } private string _uid = string.Empty;
public string UniqueIdentifier { get; set; } public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); }
public string DisplayName { get; set; } public string DisplayName { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string UserModelId { get; set; } public string UserModelId { get; set; }
@ -317,7 +297,6 @@ namespace Flow.Launcher.Plugin.Program.Programs
public Application() { } public Application() { }
public Result Result(string query, IPublicAPI api) public Result Result(string query, IPublicAPI api)
{ {
string title; string title;
@ -544,7 +523,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
} }
} }
public string FormattedPriReferenceValue(string packageName, string rawPriReferenceValue) public static string FormattedPriReferenceValue(string packageName, string rawPriReferenceValue)
{ {
const string prefix = "ms-resource:"; const string prefix = "ms-resource:";
@ -601,92 +580,97 @@ namespace Flow.Launcher.Plugin.Program.Programs
// windows 8.1 https://msdn.microsoft.com/en-us/library/windows/apps/hh965372.aspx#target_size // windows 8.1 https://msdn.microsoft.com/en-us/library/windows/apps/hh965372.aspx#target_size
// windows 8 https://msdn.microsoft.com/en-us/library/windows/apps/br211475.aspx // windows 8 https://msdn.microsoft.com/en-us/library/windows/apps/br211475.aspx
string path; string path = Path.Combine(Package.Location, uri);
if (uri.Contains("\\"))
{ var logoPath = TryToFindLogo(uri, path);
path = Path.Combine(Package.Location, uri); if (String.IsNullOrEmpty(logoPath))
}
else
{ {
// TODO: Don't know why, just keep it at the moment
// Maybe on older version of Windows 10?
// for C:\Windows\MiracastView etc // for C:\Windows\MiracastView etc
path = Path.Combine(Package.Location, "Assets", uri); return TryToFindLogo(uri, Path.Combine(Package.Location, "Assets", uri));
} }
return logoPath;
var extension = Path.GetExtension(path); string TryToFindLogo(string uri, string path)
if (extension != null)
{ {
var end = path.Length - extension.Length; var extension = Path.GetExtension(path);
var prefix = path.Substring(0, end); if (extension != null)
var paths = new List<string>
{ {
path //if (File.Exists(path))
}; //{
// return path; // shortcut, avoid enumerating files
//}
var scaleFactors = new Dictionary<PackageVersion, List<int>> var logoNamePrefix = Path.GetFileNameWithoutExtension(uri); // e.g Square44x44
{ var logoDir = Path.GetDirectoryName(path); // e.g ..\..\Assets
// scale factors on win10: https://docs.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets#asset-size-tables, if (String.IsNullOrEmpty(logoNamePrefix) || String.IsNullOrEmpty(logoDir) || !Directory.Exists(logoDir))
{ {
PackageVersion.Windows10, new List<int> // Known issue: Edge always triggers it since logo is not at uri
{ ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" +
100, $"|{UserModelId} can't find logo uri for {uri} in package location (logo name or directory not found): {Package.Location}", new FileNotFoundException());
125, return string.Empty;
150, }
200,
400 var files = Directory.EnumerateFiles(logoDir);
}
}, // Currently we don't care which one to choose
// Just ignore all qualifiers
// select like logo.[xxx_yyy].png
// https://learn.microsoft.com/en-us/windows/uwp/app-resources/tailor-resources-lang-scale-contrast
var logos = files.Where(file =>
Path.GetFileName(file)?.StartsWith(logoNamePrefix, StringComparison.OrdinalIgnoreCase) ?? false
&& extension.Equals(Path.GetExtension(file), StringComparison.OrdinalIgnoreCase)
);
var selected = logos.FirstOrDefault();
var closest = selected;
int min = int.MaxValue;
foreach(var logo in logos)
{ {
PackageVersion.Windows81, new List<int>
var imageStream = File.OpenRead(logo);
var decoder = BitmapDecoder.Create(imageStream, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.None);
var height = decoder.Frames[0].PixelHeight;
var width = decoder.Frames[0].PixelWidth;
int pixelCountDiff = Math.Abs(height * width - 1936); // 44*44=1936
if(pixelCountDiff < min)
{ {
100, // try to find the closest to 44x44 logo
120, closest = logo;
140, if (pixelCountDiff == 0)
160, break; // found 44x44
180 min = pixelCountDiff;
}
},
{
PackageVersion.Windows8, new List<int>
{
100
} }
} }
};
if (scaleFactors.ContainsKey(Package.Version)) selected = closest;
{ if (!string.IsNullOrEmpty(selected))
foreach (var factor in scaleFactors[Package.Version])
{ {
paths.Add($"{prefix}.scale-{factor}{extension}"); return selected;
}
else
{
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" +
$"|{UserModelId} can't find logo uri for {uri} in package location (can't find specified logo): {Package.Location}", new FileNotFoundException());
return string.Empty;
} }
}
var selected = paths.FirstOrDefault(File.Exists);
if (!string.IsNullOrEmpty(selected))
{
return selected;
} }
else else
{ {
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" + ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" +
$"|{UserModelId} can't find logo uri for {uri} in package location: {Package.Location}", new FileNotFoundException()); $"|Unable to find extension from {uri} for {UserModelId} " +
$"in package location {Package.Location}", new FileNotFoundException());
return string.Empty; return string.Empty;
} }
} }
else
{
ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" +
$"|Unable to find extension from {uri} for {UserModelId} " +
$"in package location {Package.Location}", new FileNotFoundException());
return string.Empty;
}
} }
public ImageSource Logo() public ImageSource Logo()
{ {
var logo = ImageFromPath(LogoPath); var logo = ImageFromPath(LogoPath);
var plated = PlatedImage(logo); var plated = PlatedImage(logo); // TODO: maybe get plated directly from app package?
// todo magic! temp fix for cross thread object // todo magic! temp fix for cross thread object
plated.Freeze(); plated.Freeze();
@ -696,9 +680,15 @@ namespace Flow.Launcher.Plugin.Program.Programs
private BitmapImage ImageFromPath(string path) private BitmapImage ImageFromPath(string path)
{ {
// TODO: Consider using infrastructure.image.imageloader?
if (File.Exists(path)) if (File.Exists(path))
{ {
var image = new BitmapImage(new Uri(path)); var image = new BitmapImage();
image.BeginInit();
image.UriSource = new Uri(path);
image.CacheOption = BitmapCacheOption.OnLoad;
image.EndInit();
image.Freeze();
return image; return image;
} }
else else
@ -770,6 +760,23 @@ namespace Flow.Launcher.Plugin.Program.Programs
{ {
return $"{DisplayName}: {Description}"; return $"{DisplayName}: {Description}";
} }
public override bool Equals(object obj)
{
if (obj is Application other)
{
return UniqueIdentifier == other.UniqueIdentifier;
}
else
{
return false;
}
}
public override int GetHashCode()
{
return UniqueIdentifier.GetHashCode();
}
} }
public enum PackageVersion public enum PackageVersion
@ -785,6 +792,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{ {
Read = 0x0, Read = 0x0,
ShareExclusive = 0x10, ShareExclusive = 0x10,
ShareDenyNone = 0x40
} }
private enum Hresult : uint private enum Hresult : uint

View file

@ -11,14 +11,10 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.Program.Logger; using Flow.Launcher.Plugin.Program.Logger;
using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Infrastructure.Logger;
using System.Collections;
using System.Diagnostics; using System.Diagnostics;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using System.Threading.Channels; using System.Threading.Channels;
using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Plugin.Program.Views.Models;
using IniParser; using IniParser;
namespace Flow.Launcher.Plugin.Program.Programs namespace Flow.Launcher.Plugin.Program.Programs
@ -27,10 +23,20 @@ namespace Flow.Launcher.Plugin.Program.Programs
public class Win32 : IProgram, IEquatable<Win32> public class Win32 : IProgram, IEquatable<Win32>
{ {
public string Name { get; set; } public string Name { get; set; }
public string UniqueIdentifier { get; set; } public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); } // For path comparison
public string IcoPath { get; set; } public string IcoPath { get; set; }
/// <summary>
/// Path of the file. It's the path of .lnk and .url for .lnk and .url files.
/// </summary>
public string FullPath { get; set; } public string FullPath { get; set; }
/// <summary>
/// Path of the excutable for .lnk, or the URL for .url. Arguments are included if any.
/// </summary>
public string LnkResolvedPath { get; set; } public string LnkResolvedPath { get; set; }
/// <summary>
/// Path of the actual executable file.
/// </summary>
public string ExecutablePath => LnkResolvedPath ?? FullPath;
public string ParentDirectory { get; set; } public string ParentDirectory { get; set; }
public string ExecutableName { get; set; } public string ExecutableName { get; set; }
public string Description { get; set; } public string Description { get; set; }
@ -41,6 +47,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
private const string ShortcutExtension = "lnk"; private const string ShortcutExtension = "lnk";
private const string UrlExtension = "url"; private const string UrlExtension = "url";
private const string ExeExtension = "exe"; private const string ExeExtension = "exe";
private string _uid = string.Empty;
private static readonly Win32 Default = new Win32() private static readonly Win32 Default = new Win32()
{ {
@ -100,10 +107,23 @@ namespace Flow.Launcher.Plugin.Program.Programs
matchResult.MatchData = new List<int>(); matchResult.MatchData = new List<int>();
} }
string subtitle = string.Empty;
if (!Main._settings.HideAppsPath)
{
if (Extension(FullPath) == UrlExtension)
{
subtitle = LnkResolvedPath;
}
else
{
subtitle = FullPath;
}
}
var result = new Result var result = new Result
{ {
Title = title, Title = title,
SubTitle = Main._settings.HideAppsPath ? string.Empty : LnkResolvedPath ?? FullPath, SubTitle = subtitle,
IcoPath = IcoPath, IcoPath = IcoPath,
Score = matchResult.Score, Score = matchResult.Score,
TitleHighlightData = matchResult.MatchData, TitleHighlightData = matchResult.MatchData,
@ -119,7 +139,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
var info = new ProcessStartInfo var info = new ProcessStartInfo
{ {
FileName = LnkResolvedPath ?? FullPath, FileName = FullPath,
WorkingDirectory = ParentDirectory, WorkingDirectory = ParentDirectory,
UseShellExecute = true, UseShellExecute = true,
Verb = runAsAdmin ? "runas" : null Verb = runAsAdmin ? "runas" : null
@ -200,8 +220,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
return Name; return Name;
} }
public static List<FileSystemWatcher> Watchers = new List<FileSystemWatcher>(); private static List<FileSystemWatcher> Watchers = new List<FileSystemWatcher>();
private static Win32 Win32Program(string path) private static Win32 Win32Program(string path)
{ {
@ -225,11 +244,17 @@ namespace Flow.Launcher.Plugin.Program.Programs
ProgramLogger.LogException($"|Win32|Win32Program|{path}" + ProgramLogger.LogException($"|Win32|Win32Program|{path}" +
$"|Permission denied when trying to load the program from {path}", e); $"|Permission denied when trying to load the program from {path}", e);
return new Win32() return Default;
{
Valid = false, Enabled = false
};
} }
#if !DEBUG
catch (Exception e)
{
ProgramLogger.LogException($"|Win32|Win32Program|{path}" +
"|An unexpected error occurred in the calling method Win32Program", e);
return Default;
}
#endif
} }
private static Win32 LnkProgram(string path) private static Win32 LnkProgram(string path)
@ -247,10 +272,15 @@ namespace Flow.Launcher.Plugin.Program.Programs
var extension = Extension(target); var extension = Extension(target);
if (extension == ExeExtension && File.Exists(target)) if (extension == ExeExtension && File.Exists(target))
{ {
program.LnkResolvedPath = program.FullPath; program.LnkResolvedPath = Path.GetFullPath(target);
program.FullPath = Path.GetFullPath(target).ToLower();
program.ExecutableName = Path.GetFileName(target); program.ExecutableName = Path.GetFileName(target);
var args = _helper.arguments;
if(!string.IsNullOrEmpty(args))
{
program.LnkResolvedPath += " " + args;
}
var description = _helper.description; var description = _helper.description;
if (!string.IsNullOrEmpty(description)) if (!string.IsNullOrEmpty(description))
{ {
@ -276,8 +306,14 @@ namespace Flow.Launcher.Plugin.Program.Programs
"|Error caused likely due to trying to get the description of the program", "|Error caused likely due to trying to get the description of the program",
e); e);
program.Valid = false; return Default;
return program; }
catch (FileNotFoundException e)
{
ProgramLogger.LogException($"|Win32|LnkProgram|{path}" +
"|An unexpected error occurred in the calling method LnkProgram", e);
return Default;
} }
#if !DEBUG //Only do a catch all in production. This is so make developer aware of any unhandled exception and add the exception handling in. #if !DEBUG //Only do a catch all in production. This is so make developer aware of any unhandled exception and add the exception handling in.
catch (Exception e) catch (Exception e)
@ -285,13 +321,12 @@ namespace Flow.Launcher.Plugin.Program.Programs
ProgramLogger.LogException($"|Win32|LnkProgram|{path}" + ProgramLogger.LogException($"|Win32|LnkProgram|{path}" +
"|An unexpected error occurred in the calling method LnkProgram", e); "|An unexpected error occurred in the calling method LnkProgram", e);
program.Valid = false; return Default;
return program;
} }
#endif #endif
} }
private static Win32 UrlProgram(string path) private static Win32 UrlProgram(string path, string[] protocols)
{ {
var program = Win32Program(path); var program = Win32Program(path);
program.Valid = false; program.Valid = false;
@ -306,9 +341,9 @@ namespace Flow.Launcher.Plugin.Program.Programs
{ {
return program; return program;
} }
foreach(var protocol in Main._settings.GetProtocols()) foreach (var protocol in protocols)
{ {
if(url.StartsWith(protocol)) if (url.StartsWith(protocol))
{ {
program.LnkResolvedPath = url; program.LnkResolvedPath = url;
program.Valid = true; program.Valid = true;
@ -340,35 +375,40 @@ namespace Flow.Launcher.Plugin.Program.Programs
program.Description = info.FileDescription; program.Description = info.FileDescription;
return program; return program;
} }
catch (FileNotFoundException e)
{
ProgramLogger.LogException($"|Win32|ExeProgram|{path}" +
$"|File not found when trying to load the program from {path}", e);
return Default;
}
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{ {
ProgramLogger.LogException($"|Win32|ExeProgram|{path}" + ProgramLogger.LogException($"|Win32|ExeProgram|{path}" +
$"|Permission denied when trying to load the program from {path}", e); $"|Permission denied when trying to load the program from {path}", e);
return new Win32() return Default;
{
Valid = false, Enabled = false
};
} }
} }
private static IEnumerable<string> ProgramPaths(string directory, string[] suffixes) private static IEnumerable<string> EnumerateProgramsInDir(string directory, string[] suffixes, bool recursive = true)
{ {
if (!Directory.Exists(directory)) if (!Directory.Exists(directory))
return Enumerable.Empty<string>(); return Enumerable.Empty<string>();
return Directory.EnumerateFiles(directory, "*", new EnumerationOptions return Directory.EnumerateFiles(directory, "*", new EnumerationOptions
{ {
IgnoreInaccessible = true, RecurseSubdirectories = true IgnoreInaccessible = true,
RecurseSubdirectories = recursive
}).Where(x => suffixes.Contains(Extension(x))); }).Where(x => suffixes.Contains(Extension(x)));
} }
private static string Extension(string path) private static string Extension(string path)
{ {
var extension = Path.GetExtension(path)?.ToLower(); var extension = Path.GetExtension(path)?.ToLowerInvariant();
if (!string.IsNullOrEmpty(extension)) if (!string.IsNullOrEmpty(extension))
{ {
return extension.Substring(1); return extension.Substring(1); // remove dot
} }
else else
{ {
@ -376,45 +416,51 @@ namespace Flow.Launcher.Plugin.Program.Programs
} }
} }
private static IEnumerable<Win32> UnregisteredPrograms(List<Settings.ProgramSource> sources, string[] suffixes) private static IEnumerable<Win32> UnregisteredPrograms(List<string> directories, string[] suffixes, string[] protocols)
{ {
var paths = ExceptDisabledSource(sources.Where(s => Directory.Exists(s.Location) && s.Enabled) // Disabled custom sources are not in DisabledProgramSources
.SelectMany(s => ProgramPaths(s.Location, suffixes)), x => x) var paths = directories.AsParallel()
.Distinct(); .SelectMany(s => EnumerateProgramsInDir(s, suffixes));
var programs = paths.Select(x => Extension(x) switch
{
ExeExtension => ExeProgram(x),
ShortcutExtension => LnkProgram(x),
UrlExtension => UrlProgram(x),
_ => Win32Program(x)
});
// Remove disabled programs in DisabledProgramSources
var programs = ExceptDisabledSource(paths).Select(x => GetProgramFromPath(x, protocols));
return programs; return programs;
} }
private static IEnumerable<Win32> StartMenuPrograms(string[] suffixes) private static IEnumerable<Win32> StartMenuPrograms(string[] suffixes, string[] protocols)
{ {
var disabledProgramsList = Main._settings.DisabledProgramSources;
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Programs); var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms); var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms);
var paths1 = ProgramPaths(directory1, suffixes); var paths1 = EnumerateProgramsInDir(directory1, suffixes);
var paths2 = ProgramPaths(directory2, suffixes); var paths2 = EnumerateProgramsInDir(directory2, suffixes);
var toFilter = paths1.Concat(paths2); var toFilter = paths1.Concat(paths2);
var programs = ExceptDisabledSource(toFilter.Distinct()) var programs = ExceptDisabledSource(toFilter.Distinct())
.Select(x => Extension(x) switch .Select(x => GetProgramFromPath(x, protocols));
{
ShortcutExtension => LnkProgram(x),
UrlExtension => UrlProgram(x),
_ => Win32Program(x)
});
return programs; return programs;
} }
private static IEnumerable<Win32> AppPathsPrograms(string[] suffixes) private static IEnumerable<Win32> PATHPrograms(string[] suffixes, string[] protocols, List<string> commonParents)
{
var pathEnv = Environment.GetEnvironmentVariable("Path");
if (String.IsNullOrEmpty(pathEnv))
{
return Array.Empty<Win32>();
}
var paths = pathEnv.Split(";", StringSplitOptions.RemoveEmptyEntries).DistinctBy(p => p.ToLowerInvariant());
var toFilter = paths.Where(x => commonParents.All(parent => !IsSubPathOf(x, parent)))
.AsParallel()
.SelectMany(p => EnumerateProgramsInDir(p, suffixes, recursive: false));
var programs = ExceptDisabledSource(toFilter.Distinct())
.Select(x => GetProgramFromPath(x, protocols));
return programs;
}
private static IEnumerable<Win32> AppPathsPrograms(string[] suffixes, string[] protocols)
{ {
// https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121 // https://msdn.microsoft.com/en-us/library/windows/desktop/ee872121
const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths"; const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths";
@ -434,12 +480,11 @@ namespace Flow.Launcher.Plugin.Program.Programs
toFilter = toFilter.Concat(GetPathFromRegistry(rootUser)); toFilter = toFilter.Concat(GetPathFromRegistry(rootUser));
} }
toFilter = toFilter.Distinct().Where(p => suffixes.Contains(Extension(p))); toFilter = toFilter.Distinct().Where(p => suffixes.Contains(Extension(p)));
var filtered = ExceptDisabledSource(toFilter); var programs = ExceptDisabledSource(toFilter)
.Select(x => GetProgramFromPath(x, protocols)).Where(x => x.Valid).ToList(); // ToList due to disposing issue
return filtered.Select(GetProgramFromPath).ToList(); // ToList due to disposing issue return programs;
} }
private static IEnumerable<string> GetPathFromRegistry(RegistryKey root) private static IEnumerable<string> GetPathFromRegistry(RegistryKey root)
@ -479,24 +524,25 @@ namespace Flow.Launcher.Plugin.Program.Programs
} }
} }
private static Win32 GetProgramFromPath(string path) private static Win32 GetProgramFromPath(string path, string[] protocols)
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
return Default; return Default;
path = Environment.ExpandEnvironmentVariables(path); path = Environment.ExpandEnvironmentVariables(path);
if (!File.Exists(path)) return Extension(path) switch
return Default; {
ShortcutExtension => LnkProgram(path),
var entry = Win32Program(path); ExeExtension => ExeProgram(path),
UrlExtension => UrlProgram(path, protocols),
return entry; _ => Win32Program(path)
}; ;
} }
public static IEnumerable<string> ExceptDisabledSource(IEnumerable<string> sources) public static IEnumerable<string> ExceptDisabledSource(IEnumerable<string> paths)
{ {
return ExceptDisabledSource(sources, x => x); return ExceptDisabledSource(paths, x => x.ToLowerInvariant());
} }
public static IEnumerable<TSource> ExceptDisabledSource<TSource>(IEnumerable<TSource> sources, public static IEnumerable<TSource> ExceptDisabledSource<TSource>(IEnumerable<TSource> sources,
@ -531,14 +577,16 @@ namespace Flow.Launcher.Plugin.Program.Programs
private static IEnumerable<Win32> ProgramsHasher(IEnumerable<Win32> programs) private static IEnumerable<Win32> ProgramsHasher(IEnumerable<Win32> programs)
{ {
return programs.GroupBy(p => p.FullPath.ToLower()) // TODO: Unable to distinguish multiple lnks to the same excutable but with different params
return programs.GroupBy(p => p.ExecutablePath.ToLowerInvariant())
.AsParallel()
.SelectMany(g => .SelectMany(g =>
{ {
var temp = g.Where(g => !string.IsNullOrEmpty(g.Description)).ToList(); var temp = g.Where(g => !string.IsNullOrEmpty(g.Description)).ToList();
if (temp.Any()) if (temp.Any())
return DistinctBy(temp, x => x.Description); return DistinctBy(temp, x => x.Description);
return g.Take(1); return g.Take(1);
}).ToArray(); });
} }
@ -547,26 +595,38 @@ namespace Flow.Launcher.Plugin.Program.Programs
try try
{ {
var programs = Enumerable.Empty<Win32>(); var programs = Enumerable.Empty<Win32>();
var suffixes = settings.GetSuffixes();
var protocols = settings.GetProtocols();
var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.GetSuffixes()); // Disabled custom sources are not in DisabledProgramSources
var sources = settings.ProgramSources.Where(s => Directory.Exists(s.Location) && s.Enabled).Distinct();
var commonParents = GetCommonParents(sources);
var unregistered = UnregisteredPrograms(commonParents, suffixes, protocols);
programs = programs.Concat(unregistered); programs = programs.Concat(unregistered);
var autoIndexPrograms = Enumerable.Empty<Win32>(); var autoIndexPrograms = Enumerable.Empty<Win32>(); // for single programs, not folders
if (settings.EnableRegistrySource) if (settings.EnableRegistrySource)
{ {
var appPaths = AppPathsPrograms(settings.GetSuffixes()); var appPaths = AppPathsPrograms(suffixes, protocols);
autoIndexPrograms = autoIndexPrograms.Concat(appPaths); autoIndexPrograms = autoIndexPrograms.Concat(appPaths);
} }
if (settings.EnableStartMenuSource) if (settings.EnableStartMenuSource)
{ {
var startMenu = StartMenuPrograms(settings.GetSuffixes()); var startMenu = StartMenuPrograms(suffixes, protocols);
autoIndexPrograms = autoIndexPrograms.Concat(startMenu); autoIndexPrograms = autoIndexPrograms.Concat(startMenu);
} }
autoIndexPrograms = ProgramsHasher(autoIndexPrograms); if (settings.EnablePATHSource)
{
var path = PATHPrograms(settings.GetSuffixes(), protocols, commonParents);
programs = programs.Concat(path);
}
autoIndexPrograms = ProgramsHasher(autoIndexPrograms).ToArray();
return programs.Concat(autoIndexPrograms).Where(x => x.Valid).Distinct().ToArray(); return programs.Concat(autoIndexPrograms).Where(x => x.Valid).Distinct().ToArray();
} }
@ -600,6 +660,18 @@ namespace Flow.Launcher.Plugin.Program.Programs
return UniqueIdentifier == other.UniqueIdentifier; return UniqueIdentifier == other.UniqueIdentifier;
} }
public override bool Equals(object obj)
{
if (obj is Win32 other)
{
return UniqueIdentifier == other.UniqueIdentifier;
}
else
{
return false;
}
}
private static IEnumerable<string> GetStartMenuPaths() private static IEnumerable<string> GetStartMenuPaths()
{ {
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Programs); var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
@ -616,11 +688,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
if (settings.EnableStartMenuSource) if (settings.EnableStartMenuSource)
paths.AddRange(GetStartMenuPaths()); paths.AddRange(GetStartMenuPaths());
paths.AddRange(from source in settings.ProgramSources where source.Enabled select source.Location); var customSources = GetCommonParents(settings.ProgramSources);
paths.AddRange(customSources);
var fileExtensionToWatch = settings.GetSuffixes();
foreach (var directory in from path in paths where Directory.Exists(path) select path) foreach (var directory in from path in paths where Directory.Exists(path) select path)
{ {
WatchDirectory(directory); WatchDirectory(directory, fileExtensionToWatch);
} }
_ = Task.Run(MonitorDirectoryChangeAsync); _ = Task.Run(MonitorDirectoryChangeAsync);
@ -640,8 +714,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
await Task.Run(Main.IndexWin32Programs); await Task.Run(Main.IndexWin32Programs);
} }
} }
public static void WatchDirectory(string directory) public static void WatchDirectory(string directory, string[] extensions)
{ {
if (!Directory.Exists(directory)) if (!Directory.Exists(directory))
{ {
@ -653,7 +727,11 @@ namespace Flow.Launcher.Plugin.Program.Programs
watcher.Deleted += static (_, _) => indexQueue.Writer.TryWrite(default); watcher.Deleted += static (_, _) => indexQueue.Writer.TryWrite(default);
watcher.EnableRaisingEvents = true; watcher.EnableRaisingEvents = true;
watcher.IncludeSubdirectories = true; watcher.IncludeSubdirectories = true;
foreach (var extension in extensions)
{
watcher.Filters.Add($"*.{extension}");
}
Watchers.Add(watcher); Watchers.Add(watcher);
} }
@ -664,5 +742,38 @@ namespace Flow.Launcher.Plugin.Program.Programs
fileSystemWatcher.Dispose(); fileSystemWatcher.Dispose();
} }
} }
// https://stackoverflow.com/a/66877016
private static bool IsSubPathOf(string subPath, string basePath)
{
var rel = Path.GetRelativePath(basePath, subPath);
return rel != "."
&& rel != ".."
&& !rel.StartsWith("../")
&& !rel.StartsWith(@"..\")
&& !Path.IsPathRooted(rel);
}
private static List<string> GetCommonParents(IEnumerable<ProgramSource> programSources)
{
// To avoid unnecessary io
// like c:\windows and c:\windows\system32
var grouped = programSources.GroupBy(p => p.Location.ToLowerInvariant()[0]); // group by disk
List<string> result = new();
foreach (var group in grouped)
{
HashSet<ProgramSource> parents = group.ToHashSet();
foreach (var source in group)
{
if (parents.Any(p => IsSubPathOf(source.Location, p.Location) &&
source != p))
{
parents.Remove(source);
}
}
result.AddRange(parents.Select(x => x.Location));
}
return result.DistinctBy(x => x.ToLowerInvariant()).ToList();
}
} }
} }

View file

@ -1,19 +1,26 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Windows.Foundation.Metadata; using Flow.Launcher.Plugin.Program.Views.Models;
namespace Flow.Launcher.Plugin.Program namespace Flow.Launcher.Plugin.Program
{ {
public class Settings public class Settings
{ {
public DateTime LastIndexTime { get; set; } public DateTime LastIndexTime { get; set; }
public List<ProgramSource> ProgramSources { get; set; } = new List<ProgramSource>();
public List<DisabledProgramSource> DisabledProgramSources { get; set; } = new List<DisabledProgramSource>();
[Obsolete, JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] /// <summary>
/// User-added program sources' directories
/// </summary>
public List<ProgramSource> ProgramSources { get; set; } = new List<ProgramSource>();
/// <summary>
/// Disabled single programs, not including User-added directories
/// </summary>
public List<ProgramSource> DisabledProgramSources { get; set; } = new List<ProgramSource>();
[Obsolete("Should use GetSuffixes() instead."), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string[] ProgramSuffixes { get; set; } = null; public string[] ProgramSuffixes { get; set; } = null;
public string[] CustomSuffixes { get; set; } = Array.Empty<string>(); // Custom suffixes only public string[] CustomSuffixes { get; set; } = Array.Empty<string>(); // Custom suffixes only
public string[] CustomProtocols { get; set; } = Array.Empty<string>(); public string[] CustomProtocols { get; set; } = Array.Empty<string>();
@ -111,6 +118,8 @@ namespace Flow.Launcher.Plugin.Program
public bool EnableDescription { get; set; } = false; public bool EnableDescription { get; set; } = false;
public bool HideAppsPath { get; set; } = true; public bool HideAppsPath { get; set; } = true;
public bool EnableRegistrySource { get; set; } = true; public bool EnableRegistrySource { get; set; } = true;
public bool EnablePATHSource { get; set; } = true;
public string CustomizedExplorer { get; set; } = Explorer; public string CustomizedExplorer { get; set; } = Explorer;
public string CustomizedArgs { get; set; } = ExplorerArgs; public string CustomizedArgs { get; set; } = ExplorerArgs;
@ -119,25 +128,5 @@ namespace Flow.Launcher.Plugin.Program
internal const string Explorer = "explorer"; internal const string Explorer = "explorer";
internal const string ExplorerArgs = "%s"; internal const string ExplorerArgs = "%s";
/// <summary>
/// Contains user added folder location contents as well as all user disabled applications
/// </summary>
/// <remarks>
/// <para>Win32 class applications set UniqueIdentifier using their full file path</para>
/// <para>UWP class applications set UniqueIdentifier using their Application User Model ID</para>
/// <para>Custom user added program sources set UniqueIdentifier using their location</para>
/// </remarks>
public class ProgramSource
{
private string name;
public string Location { get; set; }
public string Name { get => name ?? new DirectoryInfo(Location).Name; set => name = value; }
public bool Enabled { get; set; } = true;
public string UniqueIdentifier { get; set; }
}
public class DisabledProgramSource : ProgramSource { }
} }
} }

View file

@ -1,146 +1,88 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Flow.Launcher.Plugin.Program.Views.Models; using Flow.Launcher.Plugin.Program.Views.Models;
namespace Flow.Launcher.Plugin.Program.Views.Commands namespace Flow.Launcher.Plugin.Program.Views.Commands
{ {
internal static class ProgramSettingDisplay internal static class ProgramSettingDisplay
{ {
internal static List<ProgramSource> LoadProgramSources(this List<Settings.ProgramSource> programSources) internal static List<ProgramSource> LoadProgramSources()
{ {
var list = new List<ProgramSource>();
programSources.ForEach(x => list
.Add(
new ProgramSource
{
Enabled = x.Enabled,
Location = x.Location,
Name = x.Name,
UniqueIdentifier = x.UniqueIdentifier
}
));
// Even though these are disabled, we still want to display them so users can enable later on // Even though these are disabled, we still want to display them so users can enable later on
Main._settings return Main._settings
.DisabledProgramSources .DisabledProgramSources
.Where(t1 => !Main._settings .Union(Main._settings.ProgramSources)
.ProgramSources // program sourcces added above already, so exlcude .ToList();
.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier))
.Select(x => x)
.ToList()
.ForEach(x => list
.Add(
new ProgramSource
{
Enabled = x.Enabled,
Location = x.Location,
Name = x.Name,
UniqueIdentifier = x.UniqueIdentifier
}
));
return list;
} }
internal static void LoadAllApplications(this List<ProgramSource> list) internal static void DisplayAllPrograms()
{ {
Main._win32s var win32 = Main._win32s
.Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)) .Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier))
.ToList() .Select(x => new ProgramSource(x));
.ForEach(t1 => ProgramSetting.ProgramSettingDisplayList
.Add(
new ProgramSource
{
Name = t1.Name,
Location = t1.ParentDirectory,
UniqueIdentifier = t1.UniqueIdentifier,
Enabled = t1.Enabled
}
));
Main._uwps var uwp = Main._uwps
.Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)) .Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier))
.ToList() .Select(x => new ProgramSource(x));
.ForEach(t1 => ProgramSetting.ProgramSettingDisplayList
.Add( ProgramSetting.ProgramSettingDisplayList.AddRange(win32);
new ProgramSource ProgramSetting.ProgramSettingDisplayList.AddRange(uwp);
{
Name = t1.DisplayName,
Location = t1.Package.Location,
UniqueIdentifier = t1.UniqueIdentifier,
Enabled = t1.Enabled
}
));
} }
internal static void SetProgramSourcesStatus(this List<ProgramSource> list, List<ProgramSource> selectedProgramSourcesToDisable, bool status) internal static void SetProgramSourcesStatus(List<ProgramSource> selectedProgramSourcesToDisable, bool status)
{ {
ProgramSetting.ProgramSettingDisplayList foreach(var program in ProgramSetting.ProgramSettingDisplayList)
.Where(t1 => selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && t1.Enabled != status)) {
.ToList() if (selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == program.UniqueIdentifier && program.Enabled != status))
.ForEach(t1 => t1.Enabled = status); {
program.Enabled = status;
}
}
Main._win32s foreach(var program in Main._win32s)
.Where(t1 => selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && t1.Enabled != status)) {
.ToList() if (selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == program.UniqueIdentifier && program.Enabled != status))
.ForEach(t1 => t1.Enabled = status); {
program.Enabled = status;
}
}
Main._uwps foreach (var program in Main._uwps)
.Where(t1 => selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && t1.Enabled != status)) {
.ToList() if (selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == program.UniqueIdentifier && program.Enabled != status))
.ForEach(t1 => t1.Enabled = status); {
program.Enabled = status;
}
}
} }
internal static void StoreDisabledInSettings(this List<ProgramSource> list) internal static void StoreDisabledInSettings()
{ {
Main._settings.ProgramSources // Disabled, not in DisabledProgramSources or ProgramSources
.Where(t1 => ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && !x.Enabled)) var tmp = ProgramSetting.ProgramSettingDisplayList
.ToList()
.ForEach(t1 => t1.Enabled = false);
ProgramSetting.ProgramSettingDisplayList
.Where(t1 => !t1.Enabled .Where(t1 => !t1.Enabled
&& !Main._settings.DisabledProgramSources.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)) && !Main._settings.DisabledProgramSources.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)
.ToList() && !Main._settings.ProgramSources.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier));
.ForEach(x => Main._settings.DisabledProgramSources
.Add( Main._settings.DisabledProgramSources.AddRange(tmp);
new Settings.DisabledProgramSource
{
Name = x.Name,
Location = x.Location,
UniqueIdentifier = x.UniqueIdentifier,
Enabled = false
}
));
} }
internal static void RemoveDisabledFromSettings(this List<ProgramSource> list) internal static void RemoveDisabledFromSettings()
{ {
Main._settings.ProgramSources Main._settings.DisabledProgramSources.RemoveAll(t1 => t1.Enabled);
.Where(t1 => ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && x.Enabled))
.ToList()
.ForEach(t1 => t1.Enabled = true);
Main._settings.DisabledProgramSources
.Where(t1 => ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier && x.Enabled))
.ToList()
.ForEach(x => Main._settings.DisabledProgramSources.Remove(x));
} }
internal static bool IsReindexRequired(this List<ProgramSource> selectedItems) internal static bool IsReindexRequired(this List<ProgramSource> selectedItems)
{ {
if (selectedItems.Where(t1 => t1.Enabled && !Main._uwps.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)).Count() > 0 // Not in cache
&& selectedItems.Where(t1 => t1.Enabled && !Main._win32s.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)).Count() > 0) if (selectedItems.Any(t1 => t1.Enabled && !Main._uwps.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier))
&& selectedItems.Any(t1 => t1.Enabled && !Main._win32s.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)))
return true; return true;
// ProgramSources holds list of user added directories, // ProgramSources holds list of user added directories,
// so when we enable/disable we need to reindex to show/not show the programs // so when we enable/disable we need to reindex to show/not show the programs
// that are found in those directories. // that are found in those directories.
if (selectedItems.Where(t1 => Main._settings.ProgramSources.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)).Count() > 0) if (selectedItems.Any(t1 => Main._settings.ProgramSources.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)))
return true; return true;
return false; return false;

View file

@ -1,5 +1,89 @@
 using System;
using System.IO;
using System.Text.Json.Serialization;
using Flow.Launcher.Plugin.Program.Programs;
namespace Flow.Launcher.Plugin.Program.Views.Models namespace Flow.Launcher.Plugin.Program.Views.Models
{ {
public class ProgramSource : Settings.ProgramSource { } /// <summary>
/// Contains user added folder location contents as well as all user disabled applications
/// </summary>
/// <remarks>
/// <para>Win32 class applications set UniqueIdentifier using their full file path</para>
/// <para>UWP class applications set UniqueIdentifier using their Application User Model ID</para>
/// <para>Custom user added program sources set UniqueIdentifier using their location</para>
/// </remarks>
public class ProgramSource
{
private string name = string.Empty;
private string loc = string.Empty;
private string uniqueIdentifier = string.Empty;
public string Location
{
get => loc;
set
{
loc = value ?? string.Empty;
UniqueIdentifier = value;
}
}
public string Name { get => name; set => name = value ?? string.Empty; }
public bool Enabled { get; set; } = true;
public string UniqueIdentifier
{
get => uniqueIdentifier;
private set
{
uniqueIdentifier = value == null ? string.Empty : value.ToLowerInvariant();
}
}
[JsonConstructor]
public ProgramSource(string name, string location, bool enabled, string uniqueIdentifier)
{
loc = location ?? string.Empty;
Name = name;
Enabled = enabled;
UniqueIdentifier = uniqueIdentifier;
}
/// <summary>
/// Add source by location
/// </summary>
/// <param name="location">location of program source</param>
/// <param name="enabled">enabled</param>
public ProgramSource(string location, bool enabled = true)
{
loc = location ?? string.Empty;
Enabled = enabled;
UniqueIdentifier = location; // For path comparison
Name = new DirectoryInfo(Location).Name;
}
public ProgramSource(IProgram source)
{
loc = source.Location ?? string.Empty;
Name = source.Name;
Enabled = source.Enabled;
UniqueIdentifier = source.UniqueIdentifier;
}
public override bool Equals(object obj)
{
return obj is ProgramSource other && other.UniqueIdentifier == this.UniqueIdentifier;
}
public bool Equals(IProgram program)
{
return program != null && program.UniqueIdentifier == this.UniqueIdentifier;
}
public override int GetHashCode()
{
return HashCode.Combine(UniqueIdentifier);
}
}
} }

View file

@ -10,58 +10,89 @@
mc:Ignorable="d"> mc:Ignorable="d">
<Grid Margin="0"> <Grid Margin="0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="170" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="60" /> <RowDefinition Height="60" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<DockPanel
Margin="70,10,0,8"
HorizontalAlignment="Stretch"
LastChildFill="True">
<TextBlock
MinWidth="120"
Margin="0,5,10,0"
Text="{DynamicResource flowlauncher_plugin_program_index_source}" />
<WrapPanel
Width="Auto"
Margin="0,0,14,0"
HorizontalAlignment="Right"
DockPanel.Dock="Right">
<CheckBox
Name="StartMenuEnabled"
Margin="12,0,12,0"
Content="{DynamicResource flowlauncher_plugin_program_index_start}"
IsChecked="{Binding EnableStartMenuSource}"
ToolTip="{DynamicResource flowlauncher_plugin_program_index_start_tooltip}" />
<CheckBox
Name="RegistryEnabled"
Margin="12,0,12,0"
Content="{DynamicResource flowlauncher_plugin_program_index_registry}"
IsChecked="{Binding EnableRegistrySource}"
ToolTip="{DynamicResource flowlauncher_plugin_program_index_registry_tooltip}" />
<CheckBox
Name="PATHEnabled"
Margin="12,0,12,0"
Content="{DynamicResource flowlauncher_plugin_program_index_PATH}"
IsChecked="{Binding EnablePATHSource}"
ToolTip="{DynamicResource flowlauncher_plugin_program_index_PATH_tooltip}" />
</WrapPanel>
</DockPanel>
<StackPanel <StackPanel
Grid.Row="0" Grid.Row="1"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Orientation="Vertical"> Orientation="Vertical">
<StackPanel Width="Auto" Orientation="Vertical"> <Separator
<StackPanel Width="Auto" Orientation="Horizontal"> Height="1"
<CheckBox BorderBrush="{DynamicResource Color03B}"
Name="StartMenuEnabled" BorderThickness="1" />
Width="220" <DockPanel
Margin="70,8,10,8" Margin="70,10,0,8"
Content="{DynamicResource flowlauncher_plugin_program_index_start}" HorizontalAlignment="Stretch"
IsChecked="{Binding EnableStartMenuSource}" LastChildFill="True">
ToolTip="{DynamicResource flowlauncher_plugin_program_index_start_tooltip}" /> <TextBlock
<CheckBox MinWidth="120"
Name="RegistryEnabled" Margin="0,5,10,0"
Margin="70,8,10,8" Text="{DynamicResource flowlauncher_plugin_program_index_option}" />
Content="{DynamicResource flowlauncher_plugin_program_index_registry}" <WrapPanel
IsChecked="{Binding EnableRegistrySource}" Width="Auto"
ToolTip="{DynamicResource flowlauncher_plugin_program_index_registry_tooltip}" /> Margin="0,0,14,0"
</StackPanel> HorizontalAlignment="Right"
DockPanel.Dock="Right">
<Separator
Height="1"
BorderBrush="{DynamicResource Color03B}"
BorderThickness="1" />
<StackPanel Width="Auto" Orientation="Horizontal">
<CheckBox <CheckBox
Name="HideLnkEnabled" Name="HideLnkEnabled"
Width="220" Margin="12,0,12,0"
Margin="70,8,10,8"
Content="{DynamicResource flowlauncher_plugin_program_enable_hidelnkpath}" Content="{DynamicResource flowlauncher_plugin_program_enable_hidelnkpath}"
IsChecked="{Binding HideAppsPath}" IsChecked="{Binding HideAppsPath}"
ToolTip="{DynamicResource flowlauncher_plugin_program_enable_hidelnkpath_tooltip}" /> ToolTip="{DynamicResource flowlauncher_plugin_program_enable_hidelnkpath_tooltip}" />
<CheckBox <CheckBox
Name="DescriptionEnabled" Name="DescriptionEnabled"
Margin="70,8,10,8" Margin="12,0,12,0"
Content="{DynamicResource flowlauncher_plugin_program_enable_description}" Content="{DynamicResource flowlauncher_plugin_program_enable_description}"
IsChecked="{Binding EnableDescription}" IsChecked="{Binding EnableDescription}"
ToolTip="{DynamicResource flowlauncher_plugin_program_enable_description_tooltip}" /> ToolTip="{DynamicResource flowlauncher_plugin_program_enable_description_tooltip}" />
</StackPanel> </WrapPanel>
<Separator </DockPanel>
Height="1" <Separator
BorderBrush="{DynamicResource Color03B}" Height="1"
BorderThickness="1" /> BorderBrush="{DynamicResource Color03B}"
</StackPanel> BorderThickness="1" />
<StackPanel <StackPanel
Width="Auto" Width="Auto"
Margin="10,6,0,0" Height="55"
Margin="60,6,0,0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Orientation="Horizontal"> Orientation="Horizontal">
<Button <Button
@ -107,15 +138,17 @@
</StackPanel> </StackPanel>
<ListView <ListView
x:Name="programSourceView" x:Name="programSourceView"
Grid.Row="1" Grid.Row="2"
Margin="20,0,20,0" Margin="70,0,20,0"
AllowDrop="True" AllowDrop="True"
BorderBrush="DarkGray" BorderBrush="DarkGray"
BorderThickness="1" BorderThickness="1"
DragEnter="programSourceView_DragEnter" DragEnter="programSourceView_DragEnter"
Drop="programSourceView_Drop" Drop="programSourceView_Drop"
GridViewColumnHeader.Click="GridViewColumnHeaderClickedHandler" GridViewColumnHeader.Click="GridViewColumnHeaderClickedHandler"
MouseDoubleClick="programSourceView_MouseDoubleClick"
PreviewMouseRightButtonUp="ProgramSourceView_PreviewMouseRightButtonUp" PreviewMouseRightButtonUp="ProgramSourceView_PreviewMouseRightButtonUp"
SelectionChanged="programSourceView_SelectionChanged"
SelectionMode="Extended"> SelectionMode="Extended">
<ListView.View> <ListView.View>
<GridView> <GridView>
@ -126,7 +159,7 @@
</DataTemplate> </DataTemplate>
</GridViewColumn.CellTemplate> </GridViewColumn.CellTemplate>
</GridViewColumn> </GridViewColumn>
<GridViewColumn Header="{DynamicResource flowlauncher_plugin_program_enable}"> <GridViewColumn Header="{DynamicResource flowlauncher_plugin_program_enabled}">
<GridViewColumn.CellTemplate> <GridViewColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<TextBlock <TextBlock
@ -147,7 +180,7 @@
</ListView.View> </ListView.View>
</ListView> </ListView>
<DockPanel <DockPanel
Grid.Row="2" Grid.Row="3"
Grid.RowSpan="1" Grid.RowSpan="1"
Margin="0,0,20,10"> Margin="0,0,20,10">
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal"> <StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
@ -173,66 +206,3 @@
</DockPanel> </DockPanel>
</Grid> </Grid>
</UserControl> </UserControl>

View file

@ -1,7 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
@ -36,6 +35,7 @@ namespace Flow.Launcher.Plugin.Program.Views
_settings.EnableDescription = value; _settings.EnableDescription = value;
} }
} }
public bool HideAppsPath public bool HideAppsPath
{ {
get => _settings.HideAppsPath; get => _settings.HideAppsPath;
@ -66,6 +66,16 @@ namespace Flow.Launcher.Plugin.Program.Views
} }
} }
public bool EnablePATHSource
{
get => _settings.EnablePATHSource;
set
{
_settings.EnablePATHSource = value;
ReIndexing();
}
}
public string CustomizedExplorerPath public string CustomizedExplorerPath
{ {
get => _settings.CustomizedExplorer; get => _settings.CustomizedExplorer;
@ -88,7 +98,7 @@ namespace Flow.Launcher.Plugin.Program.Views
private void Setting_Loaded(object sender, RoutedEventArgs e) private void Setting_Loaded(object sender, RoutedEventArgs e)
{ {
ProgramSettingDisplayList = _settings.ProgramSources.LoadProgramSources(); ProgramSettingDisplayList = ProgramSettingDisplay.LoadProgramSources();
programSourceView.ItemsSource = ProgramSettingDisplayList; programSourceView.ItemsSource = ProgramSettingDisplayList;
ViewRefresh(); ViewRefresh();
@ -147,20 +157,35 @@ namespace Flow.Launcher.Plugin.Program.Views
private void btnEditProgramSource_OnClick(object sender, RoutedEventArgs e) private void btnEditProgramSource_OnClick(object sender, RoutedEventArgs e)
{ {
var selectedProgramSource = programSourceView.SelectedItem as Settings.ProgramSource; var selectedProgramSource = programSourceView.SelectedItem as ProgramSource;
if (selectedProgramSource != null) EditProgramSource(selectedProgramSource);
{ }
var add = new AddProgramSource(selectedProgramSource, _settings);
if (add.ShowDialog() ?? false) private void EditProgramSource(ProgramSource selectedProgramSource)
{ {
ReIndexing(); if (selectedProgramSource == null)
}
}
else
{ {
string msg = context.API.GetTranslation("flowlauncher_plugin_program_pls_select_program_source"); string msg = context.API.GetTranslation("flowlauncher_plugin_program_pls_select_program_source");
MessageBox.Show(msg); MessageBox.Show(msg);
} }
else
{
var add = new AddProgramSource(context, _settings, selectedProgramSource);
if (add.ShowDialog() ?? false)
{
if (selectedProgramSource.Enabled)
{
ProgramSettingDisplay.SetProgramSourcesStatus(new List<ProgramSource> { selectedProgramSource }, true); // sync status in win32, uwp and disabled
ProgramSettingDisplay.RemoveDisabledFromSettings();
}
else
{
ProgramSettingDisplay.SetProgramSourcesStatus(new List<ProgramSource> { selectedProgramSource }, false);
ProgramSettingDisplay.StoreDisabledInSettings();
}
ReIndexing();
}
}
} }
private void btnReindex_Click(object sender, RoutedEventArgs e) private void btnReindex_Click(object sender, RoutedEventArgs e)
@ -199,19 +224,16 @@ namespace Flow.Launcher.Plugin.Program.Views
{ {
foreach (string directory in directories) foreach (string directory in directories)
{ {
if (Directory.Exists(directory) && !ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == directory)) if (Directory.Exists(directory)
&& !ProgramSettingDisplayList.Any(x => x.UniqueIdentifier.Equals(directory, System.StringComparison.OrdinalIgnoreCase)))
{ {
var source = new ProgramSource var source = new ProgramSource(directory);
{
Location = directory,
UniqueIdentifier = directory
};
directoriesToAdd.Add(source); directoriesToAdd.Add(source);
} }
} }
if (directoriesToAdd.Count() > 0) if (directoriesToAdd.Count > 0)
{ {
directoriesToAdd.ForEach(x => _settings.ProgramSources.Add(x)); directoriesToAdd.ForEach(x => _settings.ProgramSources.Add(x));
directoriesToAdd.ForEach(x => ProgramSettingDisplayList.Add(x)); directoriesToAdd.ForEach(x => ProgramSettingDisplayList.Add(x));
@ -224,7 +246,7 @@ namespace Flow.Launcher.Plugin.Program.Views
private void btnLoadAllProgramSource_OnClick(object sender, RoutedEventArgs e) private void btnLoadAllProgramSource_OnClick(object sender, RoutedEventArgs e)
{ {
ProgramSettingDisplayList.LoadAllApplications(); ProgramSettingDisplay.DisplayAllPrograms();
ViewRefresh(); ViewRefresh();
} }
@ -235,18 +257,14 @@ namespace Flow.Launcher.Plugin.Program.Views
.SelectedItems.Cast<ProgramSource>() .SelectedItems.Cast<ProgramSource>()
.ToList(); .ToList();
if (selectedItems.Count() == 0) if (selectedItems.Count == 0)
{ {
string msg = context.API.GetTranslation("flowlauncher_plugin_program_pls_select_program_source"); string msg = context.API.GetTranslation("flowlauncher_plugin_program_pls_select_program_source");
MessageBox.Show(msg); MessageBox.Show(msg);
return; return;
} }
if (selectedItems if (IsAllItemsUserAdded(selectedItems))
.Where(t1 => !_settings
.ProgramSources
.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier))
.Count() == 0)
{ {
var msg = string.Format(context.API.GetTranslation("flowlauncher_plugin_program_delete_program_source")); var msg = string.Format(context.API.GetTranslation("flowlauncher_plugin_program_delete_program_source"));
@ -257,17 +275,17 @@ namespace Flow.Launcher.Plugin.Program.Views
DeleteProgramSources(selectedItems); DeleteProgramSources(selectedItems);
} }
else if (IsSelectedRowStatusEnabledMoreOrEqualThanDisabled(selectedItems)) else if (HasMoreOrEqualEnabledItems(selectedItems))
{ {
ProgramSettingDisplayList.SetProgramSourcesStatus(selectedItems, false); ProgramSettingDisplay.SetProgramSourcesStatus(selectedItems, false);
ProgramSettingDisplayList.StoreDisabledInSettings(); ProgramSettingDisplay.StoreDisabledInSettings();
} }
else else
{ {
ProgramSettingDisplayList.SetProgramSourcesStatus(selectedItems, true); ProgramSettingDisplay.SetProgramSourcesStatus(selectedItems, true);
ProgramSettingDisplayList.RemoveDisabledFromSettings(); ProgramSettingDisplay.RemoveDisabledFromSettings();
} }
if (selectedItems.IsReindexRequired()) if (selectedItems.IsReindexRequired())
@ -329,35 +347,41 @@ namespace Flow.Launcher.Plugin.Program.Views
dataView.Refresh(); dataView.Refresh();
} }
private bool IsSelectedRowStatusEnabledMoreOrEqualThanDisabled(List<ProgramSource> selectedItems) private static bool HasMoreOrEqualEnabledItems(List<ProgramSource> items)
{ {
return selectedItems.Where(x => x.Enabled).Count() >= selectedItems.Where(x => !x.Enabled).Count(); var enableCount = items.Where(x => x.Enabled).Count();
return enableCount >= items.Count - enableCount;
} }
private void Row_OnClick(object sender, RoutedEventArgs e) private void programSourceView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{ {
var selectedItems = programSourceView var selectedItems = programSourceView
.SelectedItems.Cast<ProgramSource>() .SelectedItems.Cast<ProgramSource>()
.ToList(); .ToList();
if (selectedItems if (IsAllItemsUserAdded(selectedItems))
.Where(t1 => !_settings
.ProgramSources
.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier))
.Count() == 0)
{ {
btnProgramSourceStatus.Content = context.API.GetTranslation("flowlauncher_plugin_program_delete"); btnProgramSourceStatus.Content = context.API.GetTranslation("flowlauncher_plugin_program_delete");
return;
} }
else if (HasMoreOrEqualEnabledItems(selectedItems))
if (IsSelectedRowStatusEnabledMoreOrEqualThanDisabled(selectedItems))
{ {
btnProgramSourceStatus.Content = "Disable"; btnProgramSourceStatus.Content = context.API.GetTranslation("flowlauncher_plugin_program_disable");
} }
else else
{ {
btnProgramSourceStatus.Content = "Enable"; btnProgramSourceStatus.Content = context.API.GetTranslation("flowlauncher_plugin_program_enable");
} }
} }
private void programSourceView_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var selectedProgramSource = programSourceView.SelectedItem as ProgramSource;
EditProgramSource(selectedProgramSource);
}
private bool IsAllItemsUserAdded(List<ProgramSource> items)
{
return items.All(x => _settings.ProgramSources.Any(y => y.UniqueIdentifier == x.UniqueIdentifier));
}
} }
} }

View file

@ -4,9 +4,17 @@ init:
- ps: | - ps: |
$version = new-object System.Version $env:APPVEYOR_BUILD_VERSION $version = new-object System.Version $env:APPVEYOR_BUILD_VERSION
$env:flowVersion = "{0}.{1}.{2}" -f $version.Major, $version.Minor, $version.Build $env:flowVersion = "{0}.{1}.{2}" -f $version.Major, $version.Minor, $version.Build
if ($env:APPVEYOR_REPO_BRANCH -eq "dev")
{
$env:prereleaseTag = "{0}.{1}.{2}.{3}" -f $version.Major, $version.Minor, $version.Build, $version.Revision
}
- sc config WSearch start= auto # Starts Windows Search service- Needed for running ExplorerTest - sc config WSearch start= auto # Starts Windows Search service- Needed for running ExplorerTest
- net start WSearch - net start WSearch
cache:
- '%USERPROFILE%\.nuget\packages -> **.sln, **.csproj' # preserve nuget folder (packages) unless the solution or projects change
assembly_info: assembly_info:
patch: true patch: true
file: SolutionAssemblyInfo.cs file: SolutionAssemblyInfo.cs
@ -14,12 +22,6 @@ assembly_info:
assembly_file_version: $(flowVersion) assembly_file_version: $(flowVersion)
assembly_informational_version: $(flowVersion) assembly_informational_version: $(flowVersion)
skip_branch_with_pr: true
skip_commits:
files:
- '*.md'
image: Visual Studio 2022 image: Visual Studio 2022
platform: Any CPU platform: Any CPU
configuration: Release configuration: Release
@ -28,7 +30,9 @@ before_build:
build: build:
project: Flow.Launcher.sln project: Flow.Launcher.sln
verbosity: minimal verbosity: minimal
after_build: test_script:
- dotnet test --no-build -c Release
after_test:
- ps: .\Scripts\post_build.ps1 - ps: .\Scripts\post_build.ps1
artifacts: artifacts:
@ -51,6 +55,17 @@ deploy:
on: on:
APPVEYOR_REPO_TAG: true APPVEYOR_REPO_TAG: true
- provider: GitHub
repository: Flow-Launcher/Prereleases
release: v$(prereleaseTag)
description: 'This is the early access build of our upcoming release. All changes contained here are reviewed, tested and stable to use.\n\nSee our [release](https://github.com/Flow-Launcher/Flow.Launcher/pulls?q=is%3Aopen+is%3Apr+label%3Arelease) Pull Request for details.\n\nFor latest production release visit [here](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest)\n\nPlease report any bugs or issues over at the [main repository](https://github.com/Flow-Launcher/Flow.Launcher/issues)'
auth_token:
secure: ij4UeXUYQBDJxn2YRAAhUOjklOGVKDB87Hn5J8tKIzj13yatoI7sLM666QDQFEgv
artifact: Squirrel Installer, Portable Version, Squirrel nupkg, Squirrel RELEASES
force_update: true
on:
branch: dev
- provider: GitHub - provider: GitHub
release: v$(flowVersion) release: v$(flowVersion)
auth_token: auth_token: