diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index 0b10382ee..c7eba05fd 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -45,6 +45,22 @@ namespace Flow.Launcher.Infrastructure.Storage FilesFolders.ValidateDirectory(DirectoryPath); } + public bool Exists() + { + return File.Exists(FilePath); + } + + public void Delete() + { + foreach (var path in new[] { FilePath, BackupFilePath, TempFilePath }) + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + } + public async Task LoadAsync() { if (Data != null) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 8dc48f7f2..a2d5a665a 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -299,6 +299,10 @@ namespace Flow.Launcher.Infrastructure.UserSettings public double WindowLeft { get; set; } public double WindowTop { get; set; } + public double PreviousScreenWidth { get; set; } + public double PreviousScreenHeight { get; set; } + public double PreviousDpiX { get; set; } + public double PreviousDpiY { get; set; } /// /// Custom left position on selected monitor diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 72a990ada..fb4e4a46f 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -91,7 +91,7 @@ namespace Flow.Launcher InitSoundEffects(); DataObject.AddPastingHandler(QueryTextBox, QueryTextBox_OnPaste); - + ModernWpf.ThemeManager.Current.ActualApplicationThemeChanged += ThemeManager_ActualApplicationThemeChanged; SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; } @@ -101,6 +101,11 @@ namespace Flow.Launcher #pragma warning disable VSTHRD100 // Avoid async void methods + private void ThemeManager_ActualApplicationThemeChanged(ModernWpf.ThemeManager sender, object args) + { + _theme.RefreshFrameAsync(); + } + private void OnSourceInitialized(object sender, EventArgs e) { var handle = Win32Helper.GetWindowHandle(this, true); @@ -716,8 +721,26 @@ namespace Flow.Launcher { if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) { - Top = _settings.WindowTop; + var previousScreenWidth = _settings.PreviousScreenWidth; + var previousScreenHeight = _settings.PreviousScreenHeight; + GetDpi(out var previousDpiX, out var previousDpiY); + + _settings.PreviousScreenWidth = SystemParameters.VirtualScreenWidth; + _settings.PreviousScreenHeight = SystemParameters.VirtualScreenHeight; + GetDpi(out var currentDpiX, out var currentDpiY); + + if (previousScreenWidth != 0 && previousScreenHeight != 0 && + previousDpiX != 0 && previousDpiY != 0 && + (previousScreenWidth != SystemParameters.VirtualScreenWidth || + previousScreenHeight != SystemParameters.VirtualScreenHeight || + previousDpiX != currentDpiX || previousDpiY != currentDpiY)) + { + AdjustPositionForResolutionChange(); + return; + } + Left = _settings.WindowLeft; + Top = _settings.WindowTop; } else { @@ -730,27 +753,73 @@ namespace Flow.Launcher break; case SearchWindowAligns.CenterTop: Left = HorizonCenter(screen); - Top = 10; + Top = VerticalTop(screen); break; case SearchWindowAligns.LeftTop: Left = HorizonLeft(screen); - Top = 10; + Top = VerticalTop(screen); break; case SearchWindowAligns.RightTop: Left = HorizonRight(screen); - Top = 10; + Top = VerticalTop(screen); break; case SearchWindowAligns.Custom: - Left = Win32Helper.TransformPixelsToDIP(this, - screen.WorkingArea.X + _settings.CustomWindowLeft, 0).X; - Top = Win32Helper.TransformPixelsToDIP(this, 0, - screen.WorkingArea.Y + _settings.CustomWindowTop).Y; + var customLeft = Win32Helper.TransformPixelsToDIP(this, + screen.WorkingArea.X + _settings.CustomWindowLeft, 0); + var customTop = Win32Helper.TransformPixelsToDIP(this, 0, + screen.WorkingArea.Y + _settings.CustomWindowTop); + Left = customLeft.X; + Top = customTop.Y; break; } } } } + private void AdjustPositionForResolutionChange() + { + var screenWidth = SystemParameters.VirtualScreenWidth; + var screenHeight = SystemParameters.VirtualScreenHeight; + GetDpi(out var currentDpiX, out var currentDpiY); + + var previousLeft = _settings.WindowLeft; + var previousTop = _settings.WindowTop; + GetDpi(out var previousDpiX, out var previousDpiY); + + var widthRatio = screenWidth / _settings.PreviousScreenWidth; + var heightRatio = screenHeight / _settings.PreviousScreenHeight; + var dpiXRatio = currentDpiX / previousDpiX; + var dpiYRatio = currentDpiY / previousDpiY; + + var newLeft = previousLeft * widthRatio * dpiXRatio; + var newTop = previousTop * heightRatio * dpiYRatio; + + var screenLeft = SystemParameters.VirtualScreenLeft; + var screenTop = SystemParameters.VirtualScreenTop; + + var maxX = screenLeft + screenWidth - ActualWidth; + var maxY = screenTop + screenHeight - ActualHeight; + + Left = Math.Max(screenLeft, Math.Min(newLeft, maxX)); + Top = Math.Max(screenTop, Math.Min(newTop, maxY)); + } + + private void GetDpi(out double dpiX, out double dpiY) + { + var source = PresentationSource.FromVisual(this); + if (source != null && source.CompositionTarget != null) + { + var matrix = source.CompositionTarget.TransformToDevice; + dpiX = 96 * matrix.M11; + dpiY = 96 * matrix.M22; + } + else + { + dpiX = 96; + dpiY = 96; + } + } + private Screen SelectedScreen() { Screen screen; @@ -811,6 +880,13 @@ namespace Flow.Launcher return left; } + public double VerticalTop(Screen screen) + { + var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); + var top = dip1.Y + 10; + return top; + } + #endregion #region Window Animation @@ -1185,6 +1261,7 @@ namespace Flow.Launcher _notifyIcon?.Dispose(); animationSoundWMP?.Close(); animationSoundWPF?.Dispose(); + ModernWpf.ThemeManager.Current.ActualApplicationThemeChanged -= ThemeManager_ActualApplicationThemeChanged; SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged; } diff --git a/Flow.Launcher/SettingWindow.xaml.cs b/Flow.Launcher/SettingWindow.xaml.cs index 79bd171ed..cf84317ac 100644 --- a/Flow.Launcher/SettingWindow.xaml.cs +++ b/Flow.Launcher/SettingWindow.xaml.cs @@ -137,18 +137,40 @@ public partial class SettingWindow if (previousTop == null || previousLeft == null || !IsPositionValid(previousTop.Value, previousLeft.Value)) { - Top = WindowTop(); - Left = WindowLeft(); + SetWindowPosition(WindowTop(), WindowLeft()); } else { - Top = previousTop.Value; - Left = previousLeft.Value; + var left = _settings.SettingWindowLeft.Value; + var top = _settings.SettingWindowTop.Value; + AdjustWindowPosition(ref top, ref left); + SetWindowPosition(top, left); } WindowState = _settings.SettingWindowState; } + private void SetWindowPosition(double top, double left) + { + // Ensure window does not exceed screen boundaries + top = Math.Max(top, SystemParameters.VirtualScreenTop); + left = Math.Max(left, SystemParameters.VirtualScreenLeft); + top = Math.Min(top, SystemParameters.VirtualScreenHeight - ActualHeight); + left = Math.Min(left, SystemParameters.VirtualScreenWidth - ActualWidth); + + Top = top; + Left = left; + } + + private void AdjustWindowPosition(ref double top, ref double left) + { + // Adjust window position if it exceeds screen boundaries + top = Math.Max(top, SystemParameters.VirtualScreenTop); + left = Math.Max(left, SystemParameters.VirtualScreenLeft); + top = Math.Min(top, SystemParameters.VirtualScreenHeight - ActualHeight); + left = Math.Min(left, SystemParameters.VirtualScreenWidth - ActualWidth); + } + private static bool IsPositionValid(double top, double left) { foreach (var screen in Screen.AllScreens) diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs index 7714b5001..7fc9dcaaa 100644 --- a/Flow.Launcher/Storage/TopMostRecord.cs +++ b/Flow.Launcher/Storage/TopMostRecord.cs @@ -1,14 +1,115 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; +using System.Text.Json; using System.Text.Json.Serialization; +using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Plugin; namespace Flow.Launcher.Storage { - public class TopMostRecord + public class FlowLauncherJsonStorageTopMostRecord + { + private readonly FlowLauncherJsonStorage _topMostRecordStorage; + private readonly MultipleTopMostRecord _topMostRecord; + + public FlowLauncherJsonStorageTopMostRecord() + { +#pragma warning disable CS0618 // Type or member is obsolete + // Get old data & new data + var topMostRecordStorage = new FlowLauncherJsonStorage(); +#pragma warning restore CS0618 // Type or member is obsolete + _topMostRecordStorage = new FlowLauncherJsonStorage(); + + // Check if data exist + var oldDataExist = topMostRecordStorage.Exists(); + var newDataExist = _topMostRecordStorage.Exists(); + + // If new data exist, it means we have already migrated the old data + // So we can safely delete the old data and load the new data + if (newDataExist) + { + try + { + topMostRecordStorage.Delete(); + } + catch + { + // Ignored - Flow will delete the old data during next startup + } + _topMostRecord = _topMostRecordStorage.Load(); + } + // If new data does not exist and old data exist, we need to migrate the old data to the new data + else if (oldDataExist) + { + // Migrate old data to new data + _topMostRecord = _topMostRecordStorage.Load(); + var oldTopMostRecord = topMostRecordStorage.Load(); + if (oldTopMostRecord == null || oldTopMostRecord.records.IsEmpty) return; + foreach (var record in oldTopMostRecord.records) + { + var newValue = new ConcurrentQueue(); + newValue.Enqueue(record.Value); + _topMostRecord.records.AddOrUpdate(record.Key, newValue, (key, oldValue) => + { + oldValue.Enqueue(record.Value); + return oldValue; + }); + } + + // Delete old data and save the new data + try + { + topMostRecordStorage.Delete(); + } + catch + { + // Ignored - Flow will delete the old data during next startup + } + Save(); + } + // If both data do not exist, we just need to create a new data + else + { + _topMostRecord = _topMostRecordStorage.Load(); + } + } + + public void Save() + { + _topMostRecordStorage.Save(); + } + + public bool IsTopMost(Result result) + { + return _topMostRecord.IsTopMost(result); + } + + public int GetTopMostIndex(Result result) + { + return _topMostRecord.GetTopMostIndex(result); + } + + public void Remove(Result result) + { + _topMostRecord.Remove(result); + } + + public void AddOrUpdate(Result result) + { + _topMostRecord.AddOrUpdate(result); + } + } + + /// + /// Old data structure to support only one top most record for the same query + /// + [Obsolete("Use MultipleTopMostRecord instead. This class will be removed in future versions.")] + internal class TopMostRecord { [JsonInclude] - public ConcurrentDictionary records { get; private set; } = new ConcurrentDictionary(); + public ConcurrentDictionary records { get; private set; } = new(); internal bool IsTopMost(Result result) { @@ -39,12 +140,145 @@ namespace Flow.Launcher.Storage } } - public class Record + /// + /// New data structure to support multiple top most records for the same query + /// + internal class MultipleTopMostRecord { - public string Title { get; set; } - public string SubTitle { get; set; } - public string PluginID { get; set; } - public string RecordKey { get; set; } + [JsonInclude] + [JsonConverter(typeof(ConcurrentDictionaryConcurrentQueueConverter))] + public ConcurrentDictionary> records { get; private set; } = new(); + + internal bool IsTopMost(Result result) + { + // origin query is null when user select the context menu item directly of one item from query list + // in this case, we do not need to check if the result is top most + if (records.IsEmpty || result.OriginQuery == null || + !records.TryGetValue(result.OriginQuery.RawQuery, out var value)) + { + return false; + } + + // since this dictionary should be very small (or empty) going over it should be pretty fast. + return value.Any(record => record.Equals(result)); + } + + internal int GetTopMostIndex(Result result) + { + // origin query is null when user select the context menu item directly of one item from query list + // in this case, we do not need to check if the result is top most + if (records.IsEmpty || result.OriginQuery == null || + !records.TryGetValue(result.OriginQuery.RawQuery, out var value)) + { + return -1; + } + + // since this dictionary should be very small (or empty) going over it should be pretty fast. + // since the latter items should be more recent, we should return the smaller index for score to subtract + // which can make them more topmost + // A, B, C => 2, 1, 0 => (max - 2), (max - 1), (max - 0) + var index = 0; + foreach (var record in value) + { + if (record.Equals(result)) + { + return value.Count - 1 - index; + } + index++; + } + return -1; + } + + internal void Remove(Result result) + { + // origin query is null when user select the context menu item directly of one item from query list + // in this case, we do not need to remove the record + if (result.OriginQuery == null || + !records.TryGetValue(result.OriginQuery.RawQuery, out var value)) + { + return; + } + + // remove the record from the queue + var queue = new ConcurrentQueue(value.Where(r => !r.Equals(result))); + if (queue.IsEmpty) + { + // if the queue is empty, remove the queue from the dictionary + records.TryRemove(result.OriginQuery.RawQuery, out _); + } + else + { + // change the queue in the dictionary + records[result.OriginQuery.RawQuery] = queue; + } + } + + internal void AddOrUpdate(Result result) + { + // origin query is null when user select the context menu item directly of one item from query list + // in this case, we do not need to add or update the record + if (result.OriginQuery == null) + { + return; + } + + var record = new Record + { + PluginID = result.PluginID, + Title = result.Title, + SubTitle = result.SubTitle, + RecordKey = result.RecordKey + }; + if (!records.TryGetValue(result.OriginQuery.RawQuery, out var value)) + { + // create a new queue if it does not exist + value = new ConcurrentQueue(); + value.Enqueue(record); + records.TryAdd(result.OriginQuery.RawQuery, value); + } + else + { + // add or update the record in the queue + var queue = new ConcurrentQueue(value.Where(r => !r.Equals(result))); // make sure we don't have duplicates + queue.Enqueue(record); + records[result.OriginQuery.RawQuery] = queue; + } + } + } + + /// + /// Because ConcurrentQueue does not support serialization, we need to convert it to a List + /// + internal class ConcurrentDictionaryConcurrentQueueConverter : JsonConverter>> + { + public override ConcurrentDictionary> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var dictionary = JsonSerializer.Deserialize>>(ref reader, options); + var concurrentDictionary = new ConcurrentDictionary>(); + foreach (var kvp in dictionary) + { + concurrentDictionary.TryAdd(kvp.Key, new ConcurrentQueue(kvp.Value)); + } + return concurrentDictionary; + } + + public override void Write(Utf8JsonWriter writer, ConcurrentDictionary> value, JsonSerializerOptions options) + { + var dict = new Dictionary>(); + foreach (var kvp in value) + { + dict.Add(kvp.Key, kvp.Value.ToList()); + } + JsonSerializer.Serialize(writer, dict, options); + } + } + + internal class Record + { + public string Title { get; init; } + public string SubTitle { get; init; } + public string PluginID { get; init; } + public string RecordKey { get; init; } public bool Equals(Result r) { diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index ac339b715..72a93574b 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -39,11 +39,10 @@ namespace Flow.Launcher.ViewModel private readonly FlowLauncherJsonStorage _historyItemsStorage; private readonly FlowLauncherJsonStorage _userSelectedRecordStorage; - private readonly FlowLauncherJsonStorage _topMostRecordStorage; + private readonly FlowLauncherJsonStorageTopMostRecord _topMostRecord; private readonly History _history; private int lastHistoryIndex = 1; private readonly UserSelectedRecord _userSelectedRecord; - private readonly TopMostRecord _topMostRecord; private CancellationTokenSource _updateSource; // Used to cancel old query flows private CancellationToken _updateToken; // Used to avoid ObjectDisposedException of _updateSource.Token @@ -143,10 +142,9 @@ namespace Flow.Launcher.ViewModel _historyItemsStorage = new FlowLauncherJsonStorage(); _userSelectedRecordStorage = new FlowLauncherJsonStorage(); - _topMostRecordStorage = new FlowLauncherJsonStorage(); + _topMostRecord = new FlowLauncherJsonStorageTopMostRecord(); _history = _historyItemsStorage.Load(); _userSelectedRecord = _userSelectedRecordStorage.Load(); - _topMostRecord = _topMostRecordStorage.Load(); ContextMenu = new ResultsViewModel(Settings, this) { @@ -1823,7 +1821,7 @@ namespace Flow.Launcher.ViewModel { _historyItemsStorage.Save(); _userSelectedRecordStorage.Save(); - _topMostRecordStorage.Save(); + _topMostRecord.Save(); } /// @@ -1856,9 +1854,12 @@ namespace Flow.Launcher.ViewModel { foreach (var result in metaResults.Results) { - if (_topMostRecord.IsTopMost(result)) + var deviationIndex = _topMostRecord.GetTopMostIndex(result); + if (deviationIndex != -1) { - result.Score = Result.MaxScore; + // Adjust the score based on the result's position in the top-most list. + // A lower deviationIndex (closer to the top) results in a higher score. + result.Score = Result.MaxScore - deviationIndex; } else { diff --git a/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml index ee6b4379d..e551a7dcb 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml @@ -48,6 +48,8 @@ Please select a program source Are you sure you want to delete the selected program sources? + Please select program sources that are not added by you + Please select program sources that are added by you Another program source with the same location already exists. Program Source diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml index 5c0ba8d0b..3bf1c6ad1 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml +++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml @@ -203,6 +203,12 @@ Margin="{StaticResource SettingPanelItemLeftTopBottomMargin}" Click="btnEditProgramSource_OnClick" Content="{DynamicResource flowlauncher_plugin_program_edit}" /> +