Migrate files from Wox to PowerLauncher (#5014)

* Moved all files from Wox to Powerlauncher

* Removed Wox project

* Changed namespace for imported files

* Resolved errors for VM

* Added build dependency order

* Fixed errors in helper class

* Remove Wox files

* Fixed errors in SingleInstance class

* Fixed wox.tests

* Fixed MSI

* Removed obsolete methods from PublicAPI

* nit fixes

* Throw null exception

* Fix merge conflict
This commit is contained in:
Divyansh Srivastava
2020-07-20 11:22:03 -07:00
committed by GitHub
parent 177546bab6
commit c85cd4ac24
40 changed files with 587 additions and 1048 deletions

View File

@@ -0,0 +1,51 @@
using Microsoft.PowerLauncher.Telemetry;
using Microsoft.PowerToys.Telemetry;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Input;
using Wox.Plugin;
namespace PowerLauncher.ViewModel
{
public class ContextMenuItemViewModel : BaseModel
{
private ICommand _command;
public string PluginName { get; set; }
public string Title { get; set; }
public string Glyph { get; set; }
public string FontFamily { get; set; }
public ICommand Command {
get
{
return this._command;
}
set
{
// ICommand does not implement the INotifyPropertyChanged interface and must call OnPropertyChanged() to prevent memory leaks
if (value != this._command)
{
this._command = value;
OnPropertyChanged();
}
}
}
public Key AcceleratorKey { get; set; }
public ModifierKeys AcceleratorModifiers { get; set; }
public bool IsAcceleratorKeyEnabled { get; set; }
public void SendTelemetryEvent(LauncherResultActionEvent.TriggerType triggerType)
{
var eventData = new LauncherResultActionEvent()
{
PluginName = PluginName,
Trigger = triggerType.ToString(),
ActionName = Title
};
PowerToysTelemetry.Log.WriteEvent(eventData);
}
}
}

View File

@@ -0,0 +1,802 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Wox.Core.Plugin;
using Wox.Core.Resource;
using PowerLauncher.Helper;
using Wox.Infrastructure;
using Wox.Infrastructure.Hotkey;
using Wox.Infrastructure.Storage;
using Wox.Infrastructure.UserSettings;
using Wox.Plugin;
using Microsoft.PowerLauncher.Telemetry;
using PowerLauncher.Storage;
using Microsoft.PowerToys.Telemetry;
using interop;
using System.Globalization;
namespace PowerLauncher.ViewModel
{
public class MainViewModel : BaseModel, ISavable, IDisposable
{
#region Private Fields
private Query _lastQuery;
private static Query _emptyQuery = new Query();
private static bool _disposed;
private string _queryTextBeforeLeaveResults;
private readonly WoxJsonStorage<QueryHistory> _historyItemsStorage;
private readonly WoxJsonStorage<UserSelectedRecord> _userSelectedRecordStorage;
private readonly WoxJsonStorage<TopMostRecord> _topMostRecordStorage;
private readonly Settings _settings;
private readonly QueryHistory _history;
private readonly UserSelectedRecord _userSelectedRecord;
private readonly TopMostRecord _topMostRecord;
private CancellationTokenSource _updateSource { get; set; }
private CancellationToken _updateToken;
private bool _saved;
private HotkeyManager _hotkeyManager { get; set; }
private ushort _hotkeyHandle;
private readonly Internationalization _translator = InternationalizationManager.Instance;
#endregion
#region Constructor
public MainViewModel(Settings settings)
{
_hotkeyManager = new HotkeyManager();
_saved = false;
_queryTextBeforeLeaveResults = "";
_lastQuery = _emptyQuery;
_disposed = false;
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
_historyItemsStorage = new WoxJsonStorage<QueryHistory>();
_userSelectedRecordStorage = new WoxJsonStorage<UserSelectedRecord>();
_topMostRecordStorage = new WoxJsonStorage<TopMostRecord>();
_history = _historyItemsStorage.Load();
_userSelectedRecord = _userSelectedRecordStorage.Load();
_topMostRecord = _topMostRecordStorage.Load();
ContextMenu = new ResultsViewModel(_settings);
Results = new ResultsViewModel(_settings);
History = new ResultsViewModel(_settings);
_selectedResults = Results;
InitializeKeyCommands();
RegisterResultsUpdatedEvent();
_settings.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(Settings.Hotkey))
{
Application.Current.Dispatcher.Invoke(() =>
{
if (!string.IsNullOrEmpty(_settings.PreviousHotkey))
{
_hotkeyManager.UnregisterHotkey(_hotkeyHandle);
}
if (!string.IsNullOrEmpty(_settings.Hotkey))
{
SetHotkey(_settings.Hotkey, OnHotkey);
}
});
}
};
SetHotkey(_settings.Hotkey, OnHotkey);
SetCustomPluginHotkey();
}
private void RegisterResultsUpdatedEvent()
{
foreach (var pair in PluginManager.GetPluginsForInterface<IResultUpdated>())
{
var plugin = (IResultUpdated)pair.Plugin;
plugin.ResultsUpdated += (s, e) =>
{
Task.Run(() =>
{
PluginManager.UpdatePluginMetadata(e.Results, pair.Metadata, e.Query);
UpdateResultView(e.Results, pair.Metadata, e.Query);
}, _updateToken);
};
}
}
private void InitializeKeyCommands()
{
IgnoreCommand = new RelayCommand(_ => {});
EscCommand = new RelayCommand(_ =>
{
if (!SelectedIsFromQueryResults())
{
SelectedResults = Results;
}
else
{
MainWindowVisibility = Visibility.Collapsed;
}
});
SelectNextItemCommand = new RelayCommand(_ =>
{
SelectedResults.SelectNextResult();
});
SelectPrevItemCommand = new RelayCommand(_ =>
{
SelectedResults.SelectPrevResult();
});
SelectNextTabItemCommand = new RelayCommand(_ =>
{
SelectedResults.SelectNextTabItem();
});
SelectPrevTabItemCommand = new RelayCommand(_ =>
{
SelectedResults.SelectPrevTabItem();
});
SelectNextPageCommand = new RelayCommand(_ =>
{
SelectedResults.SelectNextPage();
});
SelectPrevPageCommand = new RelayCommand(_ =>
{
SelectedResults.SelectPrevPage();
});
SelectFirstResultCommand = new RelayCommand(_ => SelectedResults.SelectFirstResult());
StartHelpCommand = new RelayCommand(_ =>
{
Process.Start("http://doc.wox.one/");
});
OpenResultCommand = new RelayCommand(index =>
{
var results = SelectedResults;
if (index != null)
{
results.SelectedIndex = int.Parse(index.ToString(), CultureInfo.InvariantCulture);
}
if(results.SelectedItem != null)
{
//If there is a context button selected fire the action for that button before the main command.
bool didExecuteContextButton = results.SelectedItem.ExecuteSelectedContextButton();
if (!didExecuteContextButton)
{
var result = results.SelectedItem.Result;
if (result != null && result.Action != null) // SelectedItem returns null if selection is empty.
{
MainWindowVisibility = Visibility.Collapsed;
Application.Current.Dispatcher.Invoke(() =>
{
result.Action(new ActionContext
{
SpecialKeyState = KeyboardHelper.CheckModifiers()
});
});
if (SelectedIsFromQueryResults())
{
_userSelectedRecord.Add(result);
_history.Add(result.OriginQuery.RawQuery);
}
else
{
SelectedResults = Results;
}
}
}
}
});
LoadContextMenuCommand = new RelayCommand(_ =>
{
if (SelectedIsFromQueryResults())
{
SelectedResults = ContextMenu;
}
else
{
SelectedResults = Results;
}
});
LoadHistoryCommand = new RelayCommand(_ =>
{
if (SelectedIsFromQueryResults())
{
SelectedResults = History;
History.SelectedIndex = _history.Items.Count - 1;
}
else
{
SelectedResults = Results;
}
});
ClearQueryCommand = new RelayCommand(_ =>
{
if(!string.IsNullOrEmpty(QueryText))
{
ChangeQueryText(string.Empty,true);
//Push Event to UI SystemQuery has changed
OnPropertyChanged(nameof(SystemQueryText));
}
});
}
#endregion
#region ViewModel Properties
public Brush MainWindowBackground { get; set; }
public Brush MainWindowBorderBrush { get; set; }
public ResultsViewModel Results { get; private set; }
public ResultsViewModel ContextMenu { get; private set; }
public ResultsViewModel History { get; private set; }
public string SystemQueryText { get; set; } = String.Empty;
public string QueryText { get; set; } = String.Empty;
/// <summary>
/// we need move cursor to end when we manually changed query
/// but we don't want to move cursor to end when query is updated from TextBox.
/// Also we don't want to force the results to change unless explicitly told to.
/// </summary>
/// <param name="queryText"></param>
/// <param name="requery">Optional Parameter that if true, will automatically execute a query against the updated text</param>
public void ChangeQueryText(string queryText, bool requery=false)
{
SystemQueryText = queryText;
if(requery)
{
QueryText = queryText;
Query();
}
}
public bool LastQuerySelected { get; set; }
private ResultsViewModel _selectedResults;
private ResultsViewModel SelectedResults
{
get { return _selectedResults; }
set
{
_selectedResults = value;
if (SelectedIsFromQueryResults())
{
ContextMenu.Visibility = Visibility.Collapsed;
History.Visibility = Visibility.Collapsed;
ChangeQueryText(_queryTextBeforeLeaveResults);
}
else
{
Results.Visibility = Visibility.Collapsed;
_queryTextBeforeLeaveResults = QueryText;
// Because of Fody's optimization
// setter won't be called when property value is not changed.
// so we need manually call Query()
// http://stackoverflow.com/posts/25895769/revisions
if (string.IsNullOrEmpty(QueryText))
{
Query();
}
else
{
QueryText = string.Empty;
}
}
_selectedResults.Visibility = Visibility.Visible;
}
}
public Visibility ProgressBarVisibility { get; set; }
private Visibility _visibility;
public Visibility MainWindowVisibility {
get { return _visibility; }
set {
_visibility = value;
if(value == Visibility.Visible)
{
PowerToysTelemetry.Log.WriteEvent(new LauncherShowEvent());
}
else
{
PowerToysTelemetry.Log.WriteEvent(new LauncherHideEvent());
}
}
}
public ICommand IgnoreCommand { get; set; }
public ICommand EscCommand { get; set; }
public ICommand SelectNextItemCommand { get; set; }
public ICommand SelectPrevItemCommand { get; set; }
public ICommand SelectNextTabItemCommand { get; set; }
public ICommand SelectPrevTabItemCommand { 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 ClearQueryCommand { get; set; }
#endregion
public void Query()
{
if (SelectedIsFromQueryResults())
{
QueryResults();
}
else if (HistorySelected())
{
QueryHistory();
}
}
private void QueryHistory()
{
const string id = "Query History ID";
#pragma warning disable CA1308 // Normalize strings to uppercase
var query = QueryText.ToLower(CultureInfo.InvariantCulture).Trim();
#pragma warning restore CA1308 // Normalize strings to uppercase
History.Clear();
var results = new List<Result>();
foreach (var h in _history.Items)
{
var title = _translator.GetTranslation("executeQuery");
var time = _translator.GetTranslation("lastExecuteTime");
var result = new Result
{
Title = string.Format(CultureInfo.InvariantCulture, title, h.Query),
SubTitle = string.Format(CultureInfo.InvariantCulture, time, h.ExecutedDateTime),
IcoPath = "Images\\history.png",
OriginQuery = new Query { RawQuery = h.Query },
Action = _ =>
{
SelectedResults = Results;
ChangeQueryText(h.Query);
return false;
}
};
results.Add(result);
}
if (!string.IsNullOrEmpty(query))
{
var filtered = results.Where
(
r => StringMatcher.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() ||
StringMatcher.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet()
).ToList();
History.AddResults(filtered, id);
}
else
{
History.AddResults(results, id);
}
}
private void QueryResults()
{
if (!string.IsNullOrEmpty(QueryText))
{
var queryTimer = new System.Diagnostics.Stopwatch();
queryTimer.Start();
_updateSource?.Cancel();
var currentUpdateSource = new CancellationTokenSource();
_updateSource = currentUpdateSource;
var currentCancellationToken = _updateSource.Token;
_updateToken = currentCancellationToken;
ProgressBarVisibility = Visibility.Hidden;
var query = QueryBuilder.Build(QueryText.Trim(), PluginManager.NonGlobalPlugins);
if (query != null)
{
// handle the exclusiveness of plugin using action keyword
RemoveOldQueryResults(query);
_lastQuery = query;
var plugins = PluginManager.ValidPluginsForQuery(query);
Task.Run(() =>
{
// so looping will stop once it was cancelled
var parallelOptions = new ParallelOptions { CancellationToken = currentCancellationToken };
try
{
Parallel.ForEach(plugins, parallelOptions, plugin =>
{
if (!plugin.Metadata.Disabled)
{
var results = PluginManager.QueryForPlugin(plugin, query);
if (Application.Current.Dispatcher.CheckAccess())
{
UpdateResultView(results, plugin.Metadata, query);
}
else
{
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
UpdateResultView(results, plugin.Metadata, query);
}));
}
}
});
}
catch (OperationCanceledException)
{
// nothing to do here
}
// this should happen once after all queries are done so progress bar should continue
// until the end of all querying
if (currentUpdateSource == _updateSource)
{ // update to hidden if this is still the current query
ProgressBarVisibility = Visibility.Hidden;
}
queryTimer.Stop();
var queryEvent = new LauncherQueryEvent()
{
QueryTimeMs = queryTimer.ElapsedMilliseconds,
NumResults = Results.Results.Count,
QueryLength = query.RawQuery.Length
};
PowerToysTelemetry.Log.WriteEvent(queryEvent);
}, currentCancellationToken);
}
}
else
{
_updateSource?.Cancel();
_lastQuery = _emptyQuery;
Results.SelectedItem = null;
Results.Clear();
Results.Visibility = Visibility.Collapsed;
}
}
private void RemoveOldQueryResults(Query query)
{
string lastKeyword = _lastQuery.ActionKeyword;
string keyword = query.ActionKeyword;
if (string.IsNullOrEmpty(lastKeyword))
{
if (!string.IsNullOrEmpty(keyword))
{
Results.RemoveResultsExcept(PluginManager.NonGlobalPlugins[keyword].Metadata);
}
}
else
{
if (string.IsNullOrEmpty(keyword))
{
Results.RemoveResultsFor(PluginManager.NonGlobalPlugins[lastKeyword].Metadata);
}
else if (lastKeyword != keyword)
{
Results.RemoveResultsExcept(PluginManager.NonGlobalPlugins[keyword].Metadata);
}
}
}
private bool SelectedIsFromQueryResults()
{
var selected = SelectedResults == Results;
return selected;
}
private bool ContextMenuSelected()
{
var selected = SelectedResults == ContextMenu;
return selected;
}
private bool HistorySelected()
{
var selected = SelectedResults == History;
return selected;
}
#region Hotkey
private void SetHotkey(string hotkeyStr, HotkeyCallback action)
{
var hotkey = new HotkeyModel(hotkeyStr);
SetHotkey(hotkey, action);
}
private void SetHotkey(HotkeyModel hotkeyModel, HotkeyCallback action)
{
string hotkeyStr = hotkeyModel.ToString();
try
{
Hotkey hotkey = new Hotkey();
hotkey.Alt = hotkeyModel.Alt;
hotkey.Shift = hotkeyModel.Shift;
hotkey.Ctrl = hotkeyModel.Ctrl;
hotkey.Win = hotkeyModel.Win;
hotkey.Key = (byte) KeyInterop.VirtualKeyFromKey(hotkeyModel.CharKey);
_hotkeyHandle = _hotkeyManager.RegisterHotkey(hotkey, action);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception)
#pragma warning restore CA1031 // Do not catch general exception types
{
string errorMsg = string.Format(CultureInfo.InvariantCulture, InternationalizationManager.Instance.GetTranslation("registerHotkeyFailed"), hotkeyStr);
MessageBox.Show(errorMsg);
}
}
/// <summary>
/// Checks if Wox should ignore any hotkeys
/// </summary>
/// <returns></returns>
private bool ShouldIgnoreHotkeys()
{
//double if to omit calling win32 function
if (_settings.IgnoreHotkeysOnFullscreen)
if (WindowsInteropHelper.IsWindowFullscreen())
return true;
return false;
}
private void SetCustomPluginHotkey()
{
if (_settings.CustomPluginHotkeys == null) return;
foreach (CustomPluginHotkey hotkey in _settings.CustomPluginHotkeys)
{
SetHotkey(hotkey.Hotkey, () =>
{
if (ShouldIgnoreHotkeys()) return;
MainWindowVisibility = Visibility.Visible;
ChangeQueryText(hotkey.ActionKeyword);
});
}
}
private void OnHotkey()
{
Application.Current.Dispatcher.Invoke(() =>
{
if (!ShouldIgnoreHotkeys())
{
if (_settings.LastQueryMode == LastQueryMode.Empty)
{
ChangeQueryText(string.Empty);
}
else if (_settings.LastQueryMode == LastQueryMode.Preserved)
{
LastQuerySelected = true;
}
else if (_settings.LastQueryMode == LastQueryMode.Selected)
{
LastQuerySelected = false;
}
else
{
throw new ArgumentException($"wrong LastQueryMode: <{_settings.LastQueryMode}>");
}
ToggleWox();
}
});
}
private void ToggleWox()
{
if (MainWindowVisibility != Visibility.Visible)
{
MainWindowVisibility = Visibility.Visible;
}
else
{
MainWindowVisibility = Visibility.Collapsed;
}
}
#endregion
#region Public Methods
public void Save()
{
if (!_saved)
{
_historyItemsStorage.Save();
_userSelectedRecordStorage.Save();
_topMostRecordStorage.Save();
_saved = true;
}
}
/// <summary>
/// To avoid deadlock, this method should not called from main thread
/// </summary>
public void UpdateResultView(List<Result> list, PluginMetadata metadata, Query originQuery)
{
if(list == null)
{
throw new ArgumentNullException(nameof(list));
}
if (metadata == null)
{
throw new ArgumentNullException(nameof(metadata));
}
if (originQuery == null)
{
throw new ArgumentNullException(nameof(originQuery));
}
foreach (var result in list)
{
if (_topMostRecord.IsTopMost(result))
{
result.Score = int.MaxValue;
}
else
{
result.Score += _userSelectedRecord.GetSelectedCount(result) * 5;
}
}
if (originQuery.RawQuery == _lastQuery.RawQuery)
{
Results.AddResults(list, metadata.ID);
}
if (Results.Visibility != Visibility.Visible && list.Count > 0)
{
Results.Visibility = Visibility.Visible;
}
}
public void ColdStartFix()
{
// Fix Cold start for List view xaml island
List<Result> list = new List<Result>();
Result r = new Result
{
Title = "hello"
};
list.Add(r);
Results.AddResults(list, "0");
Results.Clear();
MainWindowVisibility = System.Windows.Visibility.Collapsed;
// Fix Cold start for plugins
string s = "m";
var query = QueryBuilder.Build(s.Trim(), PluginManager.NonGlobalPlugins);
var plugins = PluginManager.ValidPluginsForQuery(query);
foreach (PluginPair plugin in plugins)
{
if (!plugin.Metadata.Disabled && plugin.Metadata.Name != "Window Walker")
{
var _ = PluginManager.QueryForPlugin(plugin, query);
}
};
}
public void HandleContextMenu(Key AcceleratorKey, ModifierKeys AcceleratorModifiers)
{
var results = SelectedResults;
if (results.SelectedItem != null)
{
foreach (ContextMenuItemViewModel contextMenuItems in results.SelectedItem.ContextMenuItems)
{
if (contextMenuItems.AcceleratorKey == AcceleratorKey && contextMenuItems.AcceleratorModifiers == AcceleratorModifiers)
{
MainWindowVisibility = Visibility.Collapsed;
contextMenuItems.Command.Execute(null);
}
}
}
}
public static string GetAutoCompleteText(int index, string input, String query)
{
if (!string.IsNullOrEmpty(input) && !string.IsNullOrEmpty(query))
{
if (index == 0)
{
if (input.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) == 0)
{
// Use the same case as the input query for the matched portion of the string
return query + input.Substring(query.Length);
}
}
}
return string.Empty;
}
public static string GetSearchText(int index, String input, string query)
{
if (!string.IsNullOrEmpty(input))
{
if (index == 0 && !string.IsNullOrEmpty(query))
{
if (input.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) == 0)
{
return query + input.Substring(query.Length);
}
}
return input;
}
return string.Empty;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
if (_hotkeyHandle != 0)
{
_hotkeyManager.UnregisterHotkey(_hotkeyHandle);
}
_hotkeyManager.Dispose();
_updateSource.Dispose();
_disposed = true;
}
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#endregion
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Windows.Input;
namespace PowerLauncher.ViewModel
{
public class RelayCommand : ICommand
{
private Action<object> _action;
public RelayCommand(Action<object> action)
{
_action = action;
}
public virtual bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged
{
add { }
remove { }
}
public virtual void Execute(object parameter)
{
_action?.Invoke(parameter);
}
}
}

View File

@@ -0,0 +1,271 @@
using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System.Windows.Media;
using Wox.Core.Plugin;
using Wox.Infrastructure.Image;
using Wox.Infrastructure.Logger;
using Wox.Plugin;
using PowerLauncher.Helper;
namespace PowerLauncher.ViewModel
{
public class ResultViewModel : BaseModel
{
public enum ActivationType
{
Selection,
Hover
};
public ObservableCollection<ContextMenuItemViewModel> ContextMenuItems { get; } = new ObservableCollection<ContextMenuItemViewModel>();
public ICommand ActivateContextButtonsHoverCommand { get; set; }
public ICommand ActivateContextButtonsSelectionCommand { get; set; }
public ICommand DeactivateContextButtonsHoverCommand { get; set; }
public ICommand DeactivateContextButtonsSelectionCommand { get; set; }
public bool IsSelected { get; set; }
public bool IsHovered { get; set; }
public bool AreContextButtonsActive { get; set; }
public int ContextMenuSelectedIndex { get; set; }
const int NoSelectionIndex = -1;
public ResultViewModel(Result result)
{
if (result != null)
{
Result = result;
}
ContextMenuSelectedIndex = NoSelectionIndex;
LoadContextMenu();
ActivateContextButtonsHoverCommand = new RelayCommand(ActivateContextButtonsHoverAction);
ActivateContextButtonsSelectionCommand = new RelayCommand(ActivateContextButtonsSelectionAction);
DeactivateContextButtonsHoverCommand = new RelayCommand(DeactivateContextButtonsHoverAction);
DeactivateContextButtonsSelectionCommand = new RelayCommand(DeactivateContextButtonsSelectionAction);
}
private void ActivateContextButtonsHoverAction(object sender)
{
ActivateContextButtons(ActivationType.Hover);
}
private void ActivateContextButtonsSelectionAction(object sender)
{
ActivateContextButtons(ActivationType.Selection);
}
public void ActivateContextButtons(ActivationType activationType)
{
// Result does not contain any context menu items - we don't need to show the context menu ListView at all.
if (ContextMenuItems.Count > 0)
{
AreContextButtonsActive = true;
}
else
{
AreContextButtonsActive = false;
}
if (activationType == ActivationType.Selection)
{
IsSelected = true;
EnableContextMenuAcceleratorKeys();
}
else if (activationType == ActivationType.Hover)
{
IsHovered = true;
}
}
private void DeactivateContextButtonsHoverAction(object sender)
{
DeactivateContextButtons(ActivationType.Hover);
}
private void DeactivateContextButtonsSelectionAction(object sender)
{
DeactivateContextButtons(ActivationType.Selection);
}
public void DeactivateContextButtons(ActivationType activationType)
{
if (activationType == ActivationType.Selection)
{
IsSelected = false;
DisableContextMenuAcceleratorkeys();
}
else if (activationType == ActivationType.Hover)
{
IsHovered = false;
}
// Result does not contain any context menu items - we don't need to show the context menu ListView at all.
if (ContextMenuItems?.Count > 0)
{
AreContextButtonsActive = IsSelected || IsHovered;
}
else
{
AreContextButtonsActive = false;
}
}
public void LoadContextMenu()
{
var results = PluginManager.GetContextMenusForPlugin(Result);
ContextMenuItems.Clear();
foreach (var r in results)
{
ContextMenuItems.Add(new ContextMenuItemViewModel()
{
PluginName = r.PluginName,
Title = r.Title,
Glyph = r.Glyph,
FontFamily = r.FontFamily,
AcceleratorKey = r.AcceleratorKey,
AcceleratorModifiers = r.AcceleratorModifiers,
Command = new RelayCommand(_ =>
{
bool hideWindow = r.Action != null && r.Action(new ActionContext
{
SpecialKeyState = KeyboardHelper.CheckModifiers()
});
if (hideWindow)
{
//TODO - Do we hide the window
// MainWindowVisibility = Visibility.Collapsed;
}
})
});
}
}
private void EnableContextMenuAcceleratorKeys()
{
foreach (var i in ContextMenuItems)
{
i.IsAcceleratorKeyEnabled = true;
}
}
private void DisableContextMenuAcceleratorkeys()
{
foreach (var i in ContextMenuItems)
{
i.IsAcceleratorKeyEnabled = false;
}
}
public ImageSource Image
{
get
{
var imagePath = Result.IcoPath;
if (string.IsNullOrEmpty(imagePath) && Result.Icon != null)
{
try
{
return Result.Icon();
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception e)
#pragma warning restore CA1031 // Do not catch general exception types
{
Log.Exception($"|ResultViewModel.Image|IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", e);
imagePath = ImageLoader.ErrorIconPath;
}
}
// will get here either when icoPath has value\icon delegate is null\when had exception in delegate
return ImageLoader.Load(imagePath);
}
}
//Returns false if we've already reached the last item.
public bool SelectNextContextButton()
{
if (ContextMenuSelectedIndex == (ContextMenuItems.Count - 1))
{
ContextMenuSelectedIndex = NoSelectionIndex;
return false;
}
ContextMenuSelectedIndex++;
return true;
}
//Returns false if we've already reached the first item.
public bool SelectPrevContextButton()
{
if (ContextMenuSelectedIndex == NoSelectionIndex)
{
return false;
}
ContextMenuSelectedIndex--;
return true;
}
public void SelectLastContextButton()
{
ContextMenuSelectedIndex = ContextMenuItems.Count - 1;
}
public bool HasSelectedContextButton()
{
var isContextSelected = (ContextMenuSelectedIndex != NoSelectionIndex);
return isContextSelected;
}
/// <summary>
/// Triggers the action on the selected context button
/// </summary>
/// <returns>False if there is nothing selected, otherwise true</returns>
public bool ExecuteSelectedContextButton()
{
if (HasSelectedContextButton())
{
ContextMenuItems[ContextMenuSelectedIndex].Command.Execute(null);
return true;
}
return false;
}
public Result Result { get; }
public override bool Equals(object obj)
{
var r = obj as ResultViewModel;
if (r != null)
{
return Result.Equals(r.Result);
}
else
{
return false;
}
}
public override int GetHashCode()
{
return Result.GetHashCode();
}
public override string ToString()
{
var display = String.IsNullOrEmpty(Result.QueryTextDisplay) ? Result.Title : Result.QueryTextDisplay;
return display;
}
}
}

View File

@@ -0,0 +1,294 @@
using PowerLauncher.Helper;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using Wox.Infrastructure.UserSettings;
using Wox.Plugin;
namespace PowerLauncher.ViewModel
{
public class ResultsViewModel : BaseModel
{
#region Private Fields
public ResultCollection Results { get; }
private readonly object _addResultsLock = new object();
private readonly object _collectionLock = new object();
private readonly Settings _settings;
// private int MaxResults => _settings?.MaxResultsToShow ?? 6;
public ResultsViewModel()
{
Results = new ResultCollection();
BindingOperations.EnableCollectionSynchronization(Results, _collectionLock);
}
public ResultsViewModel(Settings settings) : this()
{
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
_settings.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(_settings.MaxResultsToShow))
{
Application.Current.Dispatcher.Invoke(() =>
{
OnPropertyChanged(nameof(MaxHeight));
});
}
};
}
#endregion
#region Properties
public int MaxHeight
{
get
{
return _settings.MaxResultsToShow * 75;
}
}
public int SelectedIndex { get; set; }
private ResultViewModel _selectedItem;
public ResultViewModel SelectedItem
{
get { return _selectedItem; }
set
{
//value can be null when selecting an item in a virtualized list
if (value != null)
{
if (_selectedItem != null)
{
_selectedItem.DeactivateContextButtons(ResultViewModel.ActivationType.Selection);
}
_selectedItem = value;
_selectedItem.ActivateContextButtons(ResultViewModel.ActivationType.Selection);
}
else
{
_selectedItem = value;
}
}
}
public Thickness Margin { get; set; }
public Visibility Visibility { get; set; } = Visibility.Hidden;
#endregion
#region Private Methods
private static int InsertIndexOf(int newScore, IList<ResultViewModel> list)
{
int index = 0;
for (; index < list.Count; index++)
{
var result = list[index];
if (newScore > result.Result.Score)
{
break;
}
}
return index;
}
private int NewIndex(int i)
{
var n = Results.Count;
if (n > 0)
{
i = (n + i) % n;
return i;
}
else
{
// SelectedIndex returns -1 if selection is empty.
return -1;
}
}
#endregion
#region Public Methods
public void SelectNextResult()
{
SelectedIndex = NewIndex(SelectedIndex + 1);
}
public void SelectPrevResult()
{
SelectedIndex = NewIndex(SelectedIndex - 1);
}
public void SelectNextPage()
{
SelectedIndex = NewIndex(SelectedIndex + _settings.MaxResultsToShow);
}
public void SelectPrevPage()
{
SelectedIndex = NewIndex(SelectedIndex - _settings.MaxResultsToShow);
}
public void SelectFirstResult()
{
SelectedIndex = NewIndex(0);
}
public void Clear()
{
Results.Clear();
}
public void RemoveResultsExcept(PluginMetadata metadata)
{
Results.RemoveAll(r => r.Result.PluginID != metadata.ID);
}
public void RemoveResultsFor(PluginMetadata metadata)
{
Results.RemoveAll(r => r.Result.PluginID == metadata.ID);
}
public void SelectNextTabItem()
{
//Do nothing if there is no selected item or we've selected the next context button
if(!SelectedItem?.SelectNextContextButton() ?? true)
{
SelectNextResult();
}
}
public void SelectPrevTabItem()
{
//Do nothing if there is no selected item or we've selected the previous context button
if (!SelectedItem?.SelectPrevContextButton() ?? true)
{
//Tabbing backwards should highlight the last item of the previous row
SelectPrevResult();
SelectedItem.SelectLastContextButton();
}
}
/// <summary>
/// To avoid deadlock, this method should not called from main thread
/// </summary>
public void AddResults(List<Result> newRawResults, string resultId)
{
lock (_addResultsLock)
{
var newResults = NewResults(newRawResults, resultId);
// update UI in one run, so it can avoid UI flickering
Results.Update(newResults);
if (Results.Count > 0)
{
Margin = new Thickness { Top = 8 };
SelectedIndex = 0;
}
else
{
Margin = new Thickness { Top = 0 };
Visibility = Visibility.Collapsed;
}
}
}
private List<ResultViewModel> NewResults(List<Result> newRawResults, string resultId)
{
var results = Results.ToList();
var newResults = newRawResults.Select(r => new ResultViewModel(r)).ToList();
var oldResults = results.Where(r => r.Result.PluginID == resultId).ToList();
// Find the same results in A (old results) and B (new newResults)
var sameResults = oldResults
.Where(t1 => newResults.Any(x => x.Result.Equals(t1.Result)))
.ToList();
// remove result of relative complement of B in A
foreach (var result in oldResults.Except(sameResults))
{
results.Remove(result);
}
// update result with B's score and index position
foreach (var sameResult in sameResults)
{
int oldIndex = results.IndexOf(sameResult);
int oldScore = results[oldIndex].Result.Score;
var newResult = newResults[newResults.IndexOf(sameResult)];
int newScore = newResult.Result.Score;
if (newScore != oldScore)
{
var oldResult = results[oldIndex];
oldResult.Result.Score = newScore;
oldResult.Result.OriginQuery = newResult.Result.OriginQuery;
results.RemoveAt(oldIndex);
int newIndex = InsertIndexOf(newScore, results);
results.Insert(newIndex, oldResult);
}
}
// insert result in relative complement of A in B
foreach (var result in newResults.Except(sameResults))
{
int newIndex = InsertIndexOf(result.Result.Score, results);
results.Insert(newIndex, result);
}
return results;
}
#endregion
#region FormattedText Dependency Property
public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
"FormattedText",
typeof(Inline),
typeof(ResultsViewModel),
new PropertyMetadata(null, FormattedTextPropertyChanged));
public static void SetFormattedText(DependencyObject textBlock, IList<int> value)
{
if (textBlock != null)
{
textBlock.SetValue(FormattedTextProperty, value);
}
}
public static Inline GetFormattedText(DependencyObject textBlock)
{
return (Inline)textBlock?.GetValue(FormattedTextProperty);
}
private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var textBlock = d as TextBlock;
if (textBlock == null) return;
var inline = (Inline)e.NewValue;
textBlock.Inlines.Clear();
if (inline == null) return;
textBlock.Inlines.Add(inline);
}
#endregion
}
}

View File

@@ -0,0 +1,45 @@
using System.Globalization;
using Wox.Core.Resource;
using Wox.Infrastructure.Storage;
using Wox.Infrastructure.UserSettings;
using Wox.Plugin;
namespace PowerLauncher.ViewModel
{
public class SettingWindowViewModel : BaseModel
{
private readonly WoxJsonStorage<Settings> _storage;
public SettingWindowViewModel()
{
_storage = new WoxJsonStorage<Settings>();
Settings = _storage.Load();
Settings.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(Settings.ActivateTimes))
{
OnPropertyChanged(nameof(ActivatedTimes));
}
};
}
public Settings Settings { get; set; }
public void Save()
{
_storage.Save();
}
#region general
private static Internationalization _translater => InternationalizationManager.Instance;
#endregion
#region about
public string ActivatedTimes => string.Format(CultureInfo.InvariantCulture, _translater.GetTranslation("about_activate_times"), Settings.ActivateTimes);
#endregion
}
}