mirror of
https://github.com/Hofknecht/SystemTrayMenu.git
synced 2024-09-07 05:00:57 +12:00
Fix wrong loading status shown (e.g. "0 elements" instead of "loading") Fix missing loading icon (animated blue circle) Adjusted alignment of the scroll bar (aligns with the separator right above)
1148 lines
42 KiB
C#
1148 lines
42 KiB
C#
// <copyright file="Menu.xaml.cs" company="PlaceholderCompany">
|
|
// Copyright (c) PlaceholderCompany. All rights reserved.
|
|
// </copyright>
|
|
|
|
namespace SystemTrayMenu.UserInterface
|
|
{
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Data;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using SystemTrayMenu.Business;
|
|
using SystemTrayMenu.DataClasses;
|
|
using SystemTrayMenu.DllImports;
|
|
using SystemTrayMenu.Properties;
|
|
using SystemTrayMenu.Utilities;
|
|
|
|
/// <summary>
|
|
/// Logic of Menu window.
|
|
/// </summary>
|
|
public partial class Menu : Window
|
|
{
|
|
private const int CornerRadius = 10;
|
|
|
|
private static readonly RoutedEvent FadeToTransparentEvent = EventManager.RegisterRoutedEvent(
|
|
nameof(FadeToTransparent), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Menu));
|
|
|
|
private static readonly RoutedEvent FadeInEvent = EventManager.RegisterRoutedEvent(
|
|
nameof(FadeIn), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Menu));
|
|
|
|
private static readonly RoutedEvent FadeOutEvent = EventManager.RegisterRoutedEvent(
|
|
nameof(FadeOut), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Menu));
|
|
|
|
private readonly string folderPath;
|
|
|
|
private bool isShellContextMenuOpen;
|
|
private bool directionToRight;
|
|
private Point lastLocation;
|
|
|
|
internal Menu(RowData? rowDataParent, string path)
|
|
{
|
|
InitializeComponent();
|
|
|
|
if (!Config.ShowDirectoryTitleAtTop)
|
|
{
|
|
txtTitle.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
if (!Config.ShowSearchBar)
|
|
{
|
|
searchPanel.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
buttonOpenFolder.Visibility = Visibility.Collapsed;
|
|
|
|
if (!Config.ShowFunctionKeySettings)
|
|
{
|
|
buttonSettings.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
if (!Config.ShowFunctionKeyRestart)
|
|
{
|
|
buttonRestart.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
folderPath = path;
|
|
RowDataParent = rowDataParent;
|
|
if (RowDataParent == null)
|
|
{
|
|
// This will be a main menu
|
|
Level = 0;
|
|
MainMenu = this;
|
|
|
|
// Use Main Menu DPI for all further calculations
|
|
Scaling.CalculateFactorByDpi(this);
|
|
|
|
// Moving the window is only supported for the main menu
|
|
MouseDown += MainMenu_MoveStart;
|
|
|
|
if (!Config.ShowFunctionKeyPinMenu)
|
|
{
|
|
buttonMenuAlwaysOpen.Visibility = Visibility.Collapsed;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// This will be a sub menu
|
|
if (ParentMenu == null)
|
|
{
|
|
// Should never happen as each parent menu must have a valid entry which's owner is set
|
|
throw new ArgumentNullException(new (nameof(ParentMenu)));
|
|
}
|
|
|
|
Level = RowDataParent.Level + 1;
|
|
MainMenu = ParentMenu.MainMenu;
|
|
RowDataParent.SubMenu = this;
|
|
|
|
buttonMenuAlwaysOpen.Visibility = Visibility.Collapsed;
|
|
buttonSettings.Visibility = Visibility.Collapsed;
|
|
buttonRestart.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
string title = new DirectoryInfo(path).Name;
|
|
if (title.Length > MenuDefines.LengthMax)
|
|
{
|
|
title = $"{title[..MenuDefines.LengthMax]}...";
|
|
}
|
|
|
|
txtTitle.Text = Title = title;
|
|
|
|
foreach (FrameworkElement control in
|
|
new List<FrameworkElement>()
|
|
{
|
|
buttonMenuAlwaysOpen,
|
|
buttonOpenFolder,
|
|
buttonSettings,
|
|
buttonRestart,
|
|
pictureBoxSearch,
|
|
pictureBoxMenuAlwaysOpen,
|
|
pictureBoxOpenFolder,
|
|
pictureBoxSettings,
|
|
pictureBoxRestart,
|
|
pictureBoxLoading,
|
|
})
|
|
{
|
|
control.Width = Scaling.Scale(control.Width);
|
|
control.Height = Scaling.Scale(control.Height);
|
|
}
|
|
|
|
labelTitle.FontSize = Scaling.ScaleFontByPoints(8.25F);
|
|
textBoxSearch.FontSize = Scaling.ScaleFontByPoints(8.25F);
|
|
labelStatus.FontSize = Scaling.ScaleFontByPoints(7F);
|
|
dgv.FontSize = Scaling.ScaleFontByPoints(9F);
|
|
|
|
textBoxSearch.TextChanged += (_, _) => TextBoxSearch_TextChanged(false);
|
|
textBoxSearch.ContextMenu = new()
|
|
{
|
|
Background = SystemColors.ControlBrush,
|
|
};
|
|
textBoxSearch.ContextMenu.Items.Add(new MenuItem()
|
|
{
|
|
Header = Translator.GetText("To cut out"),
|
|
Command = new ActionCommand((_) => textBoxSearch.Cut()),
|
|
});
|
|
textBoxSearch.ContextMenu.Items.Add(new MenuItem()
|
|
{
|
|
Header = Translator.GetText("Copy"),
|
|
Command = new ActionCommand((_) => Clipboard.SetData(DataFormats.Text, textBoxSearch.SelectedText)),
|
|
});
|
|
textBoxSearch.ContextMenu.Items.Add(new MenuItem()
|
|
{
|
|
Header = Translator.GetText("To paste"),
|
|
Command = new ActionCommand((_) =>
|
|
{
|
|
if (Clipboard.ContainsText(TextDataFormat.Text))
|
|
{
|
|
textBoxSearch.SelectedText = Clipboard.GetData(DataFormats.Text).ToString();
|
|
}
|
|
}),
|
|
});
|
|
textBoxSearch.ContextMenu.Items.Add(new MenuItem()
|
|
{
|
|
Header = Translator.GetText("Undo"),
|
|
Command = new ActionCommand((_) => textBoxSearch.Undo()),
|
|
});
|
|
textBoxSearch.ContextMenu.Items.Add(new MenuItem()
|
|
{
|
|
Header = Translator.GetText("Selecting All"),
|
|
Command = new ActionCommand((_) => textBoxSearch.SelectAll()),
|
|
});
|
|
|
|
Loaded += (_, _) =>
|
|
{
|
|
NativeMethods.HideFromAltTab(this);
|
|
|
|
RaiseEvent(new(routedEvent: FadeInEvent));
|
|
|
|
FocusTextBox();
|
|
};
|
|
|
|
Closed += (_, _) =>
|
|
{
|
|
if (RowDataParent?.SubMenu == this)
|
|
{
|
|
RowDataParent.SubMenu = null;
|
|
}
|
|
|
|
foreach (RowData item in dgv.Items.SourceCollection)
|
|
{
|
|
item.SubMenu?.Close();
|
|
}
|
|
};
|
|
}
|
|
|
|
internal event Action<RowData>? StartLoadSubMenu;
|
|
|
|
internal event Action? MenuScrolled;
|
|
|
|
internal event Action<Menu, Key, ModifierKeys>? CmdKeyProcessed;
|
|
|
|
internal event Action? SearchTextChanging;
|
|
|
|
internal event Action<Menu, bool, bool>? SearchTextChanged;
|
|
|
|
internal event Action<RowData>? RowSelectionChanged;
|
|
|
|
internal event Action<RowData>? CellMouseEnter;
|
|
|
|
internal event Action? CellMouseLeave;
|
|
|
|
internal event Action<RowData>? CellMouseDown;
|
|
|
|
internal event Action<RowData>? CellOpenOnClick;
|
|
|
|
internal event RoutedEventHandler FadeToTransparent
|
|
{
|
|
add { AddHandler(FadeToTransparentEvent, value); }
|
|
remove { RemoveHandler(FadeToTransparentEvent, value); }
|
|
}
|
|
|
|
internal event RoutedEventHandler FadeIn
|
|
{
|
|
add { AddHandler(FadeInEvent, value); }
|
|
remove { RemoveHandler(FadeInEvent, value); }
|
|
}
|
|
|
|
internal event RoutedEventHandler FadeOut
|
|
{
|
|
add { AddHandler(FadeOutEvent, value); }
|
|
remove { RemoveHandler(FadeOutEvent, value); }
|
|
}
|
|
|
|
internal enum StartLocation
|
|
{
|
|
Point,
|
|
Predecessor,
|
|
BottomLeft,
|
|
BottomRight,
|
|
TopRight,
|
|
}
|
|
|
|
internal Point Location => new (Left, Top); // TODO WPF Replace Forms wrapper
|
|
|
|
internal int Level { get; set; }
|
|
|
|
internal RowData? RowDataParent { get; set; }
|
|
|
|
internal RowData? SelectedItem
|
|
{
|
|
get => (RowData?)dgv.SelectedItem;
|
|
set => dgv.SelectedItem = value;
|
|
}
|
|
|
|
internal Menu MainMenu { get; private set; }
|
|
|
|
internal Menu? ParentMenu => RowDataParent?.Owner;
|
|
|
|
internal Menu? SubMenu
|
|
{
|
|
get
|
|
{
|
|
foreach (RowData rowData in dgv.Items.SourceCollection)
|
|
{
|
|
if (rowData.SubMenu != null)
|
|
{
|
|
return rowData.SubMenu;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
internal bool RelocateOnNextShow { get; set; } = true;
|
|
|
|
public override string ToString() => nameof(Menu) + " L" + Level.ToString() + ": " + Title;
|
|
|
|
internal void RiseItemOpened(RowData item) => CellOpenOnClick?.Invoke(item);
|
|
|
|
internal void RiseStartLoadSubMenu(RowData rowData) => StartLoadSubMenu?.Invoke(rowData);
|
|
|
|
internal void ResetSearchText()
|
|
{
|
|
textBoxSearch.Text = string.Empty;
|
|
if (dgv.Items.Count > 0)
|
|
{
|
|
dgv.ScrollIntoView(dgv.Items[0]);
|
|
}
|
|
}
|
|
|
|
internal void OnWatcherUpdate()
|
|
{
|
|
TextBoxSearch_TextChanged(true);
|
|
if (dgv.Items.Count > 0)
|
|
{
|
|
dgv.ScrollIntoView(dgv.Items[0]);
|
|
}
|
|
}
|
|
|
|
internal void FocusTextBox()
|
|
{
|
|
textBoxSearch.Select(textBoxSearch.Text.Length, 0);
|
|
textBoxSearch.Focus();
|
|
}
|
|
|
|
internal void SetSubMenuState(MenuDataDirectoryState state)
|
|
{
|
|
if (Config.ShowFunctionKeyOpenFolder)
|
|
{
|
|
buttonOpenFolder.Visibility = Visibility.Visible;
|
|
}
|
|
|
|
pictureBoxLoading.Visibility = Visibility.Collapsed;
|
|
|
|
switch (state)
|
|
{
|
|
case MenuDataDirectoryState.Valid:
|
|
if (Config.ShowCountOfElementsBelow)
|
|
{
|
|
((INotifyCollectionChanged)dgv.Items).CollectionChanged += ListView_CollectionChanged;
|
|
ListView_CollectionChanged(this, new(NotifyCollectionChangedAction.Reset));
|
|
}
|
|
else
|
|
{
|
|
labelStatus.Visibility = Visibility.Collapsed;
|
|
}
|
|
|
|
break;
|
|
case MenuDataDirectoryState.Empty:
|
|
searchPanel.Visibility = Visibility.Collapsed;
|
|
labelStatus.Content = Translator.GetText("Directory empty");
|
|
break;
|
|
case MenuDataDirectoryState.NoAccess:
|
|
searchPanel.Visibility = Visibility.Collapsed;
|
|
labelStatus.Content = Translator.GetText("Directory inaccessible");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// TODO: Check if we can just use original IsMouseOver instead? (Check if it requires Mouse.Capture(this))
|
|
internal new bool IsMouseOver()
|
|
{
|
|
Point mousePos = NativeMethods.Screen.CursorPosition;
|
|
bool isMouseOver = Visibility == Visibility.Visible &&
|
|
mousePos.X >= 0 && mousePos.X < Width &&
|
|
mousePos.Y >= 0 && mousePos.Y < Height;
|
|
return isMouseOver;
|
|
}
|
|
|
|
internal ListView GetDataGridView() => dgv; // TODO WPF Replace Forms wrapper
|
|
|
|
// Not used as refreshing should be done automatically due to databinding
|
|
// TODO: As long as WPF transition from Forms is incomplete, keep it for testing.
|
|
internal void RefreshDataGridView()
|
|
{
|
|
((CollectionView)CollectionViewSource.GetDefaultView(dgv.ItemsSource)).Refresh();
|
|
}
|
|
|
|
// TODO: Check if it is implicitly already running due to SelectionChanged event
|
|
// In case it is needed, run it within HideWithFade/ShowWithFade?
|
|
internal void RefreshSelection() => ListView_SelectionChanged(GetDataGridView(), null);
|
|
|
|
internal bool TrySelectAt(int index, int indexAlternative = -1)
|
|
{
|
|
RowData itemData;
|
|
if (index >= 0 && dgv.Items.Count > index)
|
|
{
|
|
itemData = (RowData)dgv.Items[index];
|
|
}
|
|
else if (indexAlternative >= 0 && dgv.Items.Count > indexAlternative)
|
|
{
|
|
itemData = (RowData)dgv.Items[indexAlternative];
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
dgv.SelectedItem = itemData;
|
|
dgv.ScrollIntoView(itemData);
|
|
|
|
RowSelectionChanged?.Invoke(itemData);
|
|
|
|
return true;
|
|
}
|
|
|
|
internal void AddItemsToMenu(List<RowData> data, MenuDataDirectoryState? state)
|
|
{
|
|
for (int index = 0; index < data.Count; index++)
|
|
{
|
|
RowData rowData = data[index];
|
|
rowData.RowIndex = index;
|
|
rowData.Owner = this;
|
|
rowData.SortIndex = rowData.IsAdditionalItem && Settings.Default.ShowOnlyAsSearchResult ? 99 : 0;
|
|
}
|
|
|
|
dgv.ItemsSource = data;
|
|
|
|
CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(dgv.ItemsSource);
|
|
view.Filter = (object item) => Filter_Default((RowData)item);
|
|
|
|
if (state != null)
|
|
{
|
|
SetSubMenuState(state.Value);
|
|
}
|
|
}
|
|
|
|
internal void ActivateWithFade(bool recursive)
|
|
{
|
|
if (recursive)
|
|
{
|
|
SubMenu?.ActivateWithFade(true);
|
|
}
|
|
|
|
if (Opacity != 1D)
|
|
{
|
|
if (Settings.Default.UseFading)
|
|
{
|
|
RaiseEvent(new(routedEvent: FadeInEvent));
|
|
}
|
|
else
|
|
{
|
|
Opacity = 1D;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void ShowWithFade(bool transparency, bool recursive)
|
|
{
|
|
if (recursive)
|
|
{
|
|
SubMenu?.ShowWithFade(transparency, true);
|
|
}
|
|
|
|
if (Level > 0)
|
|
{
|
|
ShowActivated = false;
|
|
}
|
|
|
|
Opacity = 0D;
|
|
Show();
|
|
|
|
if (!Settings.Default.UseFading)
|
|
{
|
|
Opacity = transparency ? 0.80D : 1D;
|
|
}
|
|
else if (transparency)
|
|
{
|
|
RaiseEvent(new(routedEvent: FadeToTransparentEvent));
|
|
}
|
|
else
|
|
{
|
|
RaiseEvent(new(routedEvent: FadeInEvent));
|
|
}
|
|
}
|
|
|
|
internal void HideAllMenus()
|
|
{
|
|
// Find main menu and close/hide all
|
|
Menu menu = this;
|
|
while (menu.ParentMenu != null)
|
|
{
|
|
menu = menu.ParentMenu;
|
|
}
|
|
|
|
menu.HideWithFade(true);
|
|
}
|
|
|
|
internal void HideWithFade(bool recursive)
|
|
{
|
|
if (recursive)
|
|
{
|
|
SubMenu?.HideWithFade(true);
|
|
}
|
|
|
|
if (RowDataParent != null)
|
|
{
|
|
RowDataParent.SubMenu = null;
|
|
}
|
|
|
|
if (Settings.Default.UseFading)
|
|
{
|
|
RaiseEvent(new(routedEvent: FadeOutEvent));
|
|
}
|
|
else
|
|
{
|
|
FadeOut_Completed(this, new());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the position and size of the menu.
|
|
/// </summary>
|
|
/// <param name="bounds">Screen coordinates where the menu is allowed to be drawn in.</param>
|
|
/// <param name="menuPredecessor">Predecessor menu (when available).</param>
|
|
/// <param name="startLocation">Defines where the first menu is drawn (when no predecessor is set).</param>
|
|
/// <param name="useCustomLocation">Use CustomLocation as start position.</param>
|
|
internal void AdjustSizeAndLocation(
|
|
Rect bounds,
|
|
Menu? menuPredecessor,
|
|
StartLocation startLocation,
|
|
bool useCustomLocation)
|
|
{
|
|
Point originLocation = new(0D, 0D);
|
|
|
|
// Update the height and width
|
|
AdjustDataGridViewHeight(menuPredecessor, bounds.Height);
|
|
AdjustDataGridViewWidth();
|
|
|
|
if (Level > 0)
|
|
{
|
|
if (menuPredecessor == null)
|
|
{
|
|
// should never happen
|
|
return;
|
|
}
|
|
|
|
// Sub Menu location depends on the location of its predecessor
|
|
startLocation = StartLocation.Predecessor;
|
|
originLocation = menuPredecessor.Location;
|
|
}
|
|
else if (useCustomLocation)
|
|
{
|
|
if (!RelocateOnNextShow)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RelocateOnNextShow = false;
|
|
startLocation = StartLocation.Point;
|
|
originLocation = new(Settings.Default.CustomLocationX, Settings.Default.CustomLocationY);
|
|
}
|
|
else if (Settings.Default.AppearAtMouseLocation)
|
|
{
|
|
if (!RelocateOnNextShow)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RelocateOnNextShow = false;
|
|
startLocation = StartLocation.Point;
|
|
originLocation = NativeMethods.Screen.CursorPosition;
|
|
}
|
|
|
|
if (IsLoaded)
|
|
{
|
|
AdjustWindowPositionInternal(originLocation);
|
|
}
|
|
else
|
|
{
|
|
// Layout cannot be calculated during loading, postpone this event
|
|
Loaded += (_, _) => AdjustWindowPositionInternal(originLocation);
|
|
}
|
|
|
|
void AdjustWindowPositionInternal(in Point originLocation)
|
|
{
|
|
double scaling = Math.Round(Scaling.Factor, 0, MidpointRounding.AwayFromZero);
|
|
double overlappingOffset = 0D;
|
|
|
|
// Make sure we have latest values of own window size
|
|
UpdateLayout();
|
|
|
|
// Prepare parameters
|
|
if (startLocation == StartLocation.Predecessor)
|
|
{
|
|
if (menuPredecessor == null)
|
|
{
|
|
// should never happen
|
|
return;
|
|
}
|
|
|
|
// When (later in calculation) a list item is not found,
|
|
// its values might be invalidated due to resizing or moving.
|
|
// After updating the layout the location should be available again.
|
|
menuPredecessor.UpdateLayout();
|
|
|
|
directionToRight = menuPredecessor.directionToRight; // try keeping same direction from predecessor
|
|
|
|
if (!Settings.Default.AppearNextToPreviousMenu &&
|
|
menuPredecessor.Width > Settings.Default.OverlappingOffsetPixels)
|
|
{
|
|
if (directionToRight)
|
|
{
|
|
overlappingOffset = Settings.Default.OverlappingOffsetPixels - menuPredecessor.Width;
|
|
}
|
|
else
|
|
{
|
|
overlappingOffset = menuPredecessor.Width - Settings.Default.OverlappingOffsetPixels;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
directionToRight = true; // use right as default direction
|
|
}
|
|
|
|
// Calculate X position
|
|
double x;
|
|
switch (startLocation)
|
|
{
|
|
case StartLocation.Point:
|
|
x = originLocation.X;
|
|
break;
|
|
case StartLocation.Predecessor:
|
|
if (menuPredecessor == null)
|
|
{
|
|
// should never happen
|
|
return;
|
|
}
|
|
|
|
if (directionToRight)
|
|
{
|
|
x = originLocation.X + menuPredecessor.Width - scaling;
|
|
|
|
// Change direction when out of bounds (predecessor only)
|
|
if (startLocation == StartLocation.Predecessor &&
|
|
bounds.X + bounds.Width <= x + Width - scaling)
|
|
{
|
|
x = originLocation.X - Width + scaling;
|
|
if (x < bounds.X &&
|
|
originLocation.X + menuPredecessor.Width < bounds.X + bounds.Width &&
|
|
bounds.X + (bounds.Width / 2) > originLocation.X + (Width / 2))
|
|
{
|
|
x = bounds.X + bounds.Width - Width + scaling;
|
|
}
|
|
else
|
|
{
|
|
if (x < bounds.X)
|
|
{
|
|
x = bounds.X;
|
|
}
|
|
|
|
directionToRight = !directionToRight;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
x = originLocation.X - Width + scaling;
|
|
|
|
// Change direction when out of bounds (predecessor only)
|
|
if (startLocation == StartLocation.Predecessor &&
|
|
x < bounds.X)
|
|
{
|
|
x = originLocation.X + menuPredecessor.Width - scaling;
|
|
if (x + Width > bounds.X + bounds.Width &&
|
|
originLocation.X > bounds.X &&
|
|
bounds.X + (bounds.Width / 2) < originLocation.X + (Width / 2))
|
|
{
|
|
x = bounds.X;
|
|
}
|
|
else
|
|
{
|
|
if (x + Width > bounds.X + bounds.Width)
|
|
{
|
|
x = bounds.X + bounds.Width - Width + scaling;
|
|
}
|
|
|
|
directionToRight = !directionToRight;
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
case StartLocation.BottomLeft:
|
|
x = bounds.X;
|
|
directionToRight = true;
|
|
break;
|
|
case StartLocation.TopRight:
|
|
case StartLocation.BottomRight:
|
|
default:
|
|
x = bounds.Width - Width;
|
|
directionToRight = false;
|
|
break;
|
|
}
|
|
|
|
x += overlappingOffset;
|
|
|
|
// Calculate Y position
|
|
double y;
|
|
switch (startLocation)
|
|
{
|
|
case StartLocation.Point:
|
|
y = originLocation.Y;
|
|
if (Settings.Default.AppearAtMouseLocation)
|
|
{
|
|
y -= labelTitle.ActualHeight; // Mouse should point below title
|
|
}
|
|
|
|
break;
|
|
case StartLocation.Predecessor:
|
|
if (menuPredecessor == null)
|
|
{
|
|
// should never happen
|
|
return;
|
|
}
|
|
|
|
y = originLocation.Y;
|
|
|
|
// Set position on same height as the selected row from predecessor
|
|
ListView dgv = menuPredecessor.GetDataGridView()!;
|
|
RowData? trigger = RowDataParent;
|
|
if (trigger != null)
|
|
{
|
|
// When scrolled, we have to reduce the index number as we calculate based on visual tree
|
|
int startIndex = 0;
|
|
double offset = 0D;
|
|
if (VisualTreeHelper.GetChild(dgv, 0) is Decorator { Child: ScrollViewer scrollViewer })
|
|
{
|
|
startIndex = (int)scrollViewer.VerticalOffset;
|
|
if (trigger.RowIndex < startIndex)
|
|
{
|
|
// calculate position above starting point
|
|
for (int i = trigger.RowIndex; i < startIndex; i++)
|
|
{
|
|
ListViewItem? item = dgv.FindVisualChildOfType<ListViewItem>(i);
|
|
if (item != null)
|
|
{
|
|
offset -= item.ActualHeight;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (startIndex < trigger.RowIndex)
|
|
{
|
|
// calculate position below starting point
|
|
// outer loop check for max RowIndex, independend of currently active filter
|
|
// inner loop check for filtered and shown items
|
|
for (int i = startIndex; i < trigger.RowIndex; i++)
|
|
{
|
|
ListViewItem? item = dgv.FindVisualChildOfType<ListViewItem>(i);
|
|
if (item != null)
|
|
{
|
|
if (((RowData)item.Content).RowIndex >= trigger.RowIndex)
|
|
{
|
|
break;
|
|
}
|
|
|
|
offset += item.ActualHeight;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (offset < 0)
|
|
{
|
|
// Do not allow to show window higher than previous window
|
|
offset = 0;
|
|
}
|
|
else
|
|
{
|
|
double offsetList = menuPredecessor.GetRelativeChildPositionTo(dgv).Y;
|
|
offsetList += dgv.ActualHeight;
|
|
if (offsetList < offset)
|
|
{
|
|
// Do not allow to show window below last entry position of list
|
|
offset = offsetList;
|
|
}
|
|
}
|
|
|
|
y += offset;
|
|
}
|
|
|
|
if (searchPanel.Visibility == Visibility.Collapsed)
|
|
{
|
|
y += menuPredecessor.searchPanel.ActualHeight;
|
|
}
|
|
|
|
break;
|
|
case StartLocation.TopRight:
|
|
y = bounds.Y;
|
|
break;
|
|
case StartLocation.BottomLeft:
|
|
case StartLocation.BottomRight:
|
|
default:
|
|
y = bounds.Height - Height;
|
|
break;
|
|
}
|
|
|
|
// Move vertically when out of bounds
|
|
if (bounds.Y + bounds.Height < y + Height)
|
|
{
|
|
y = bounds.Y + bounds.Height - Height;
|
|
}
|
|
else if (y < bounds.Y)
|
|
{
|
|
y = bounds.Y;
|
|
}
|
|
|
|
// Update position
|
|
Left = x;
|
|
Top = y;
|
|
|
|
if (Settings.Default.RoundCorners)
|
|
{
|
|
windowFrame.CornerRadius = new CornerRadius(CornerRadius);
|
|
}
|
|
|
|
UpdateLayout();
|
|
}
|
|
}
|
|
|
|
private static bool Filter_Default(RowData itemData)
|
|
{
|
|
if (Settings.Default.ShowOnlyAsSearchResult && itemData.IsAdditionalItem)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool Filter_ByUserPattern(RowData itemData, string userPattern)
|
|
{
|
|
// Instead implementing in-string wildcards, simply split into multiple search pattersy
|
|
// Look for each space separated string if it is part of an entry's text (case insensitive)
|
|
foreach (string pattern in userPattern.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
if (!itemData.ColumnText.ToLower().Contains(pattern))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void FadeOut_Completed(object sender, EventArgs e) => Hide();
|
|
|
|
private void HandlePreviewKeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
searchPanel.Visibility = Visibility.Visible;
|
|
|
|
ModifierKeys modifiers = Keyboard.Modifiers;
|
|
switch (e.Key)
|
|
{
|
|
case Key.F4:
|
|
if (modifiers != ModifierKeys.Alt)
|
|
{
|
|
return;
|
|
}
|
|
|
|
break;
|
|
case Key.F:
|
|
if (modifiers != ModifierKeys.Control)
|
|
{
|
|
return;
|
|
}
|
|
|
|
break;
|
|
case Key.Tab:
|
|
if ((modifiers != ModifierKeys.Shift) && (modifiers != ModifierKeys.None))
|
|
{
|
|
return;
|
|
}
|
|
|
|
break;
|
|
case Key.Enter:
|
|
case Key.Home:
|
|
case Key.End:
|
|
case Key.Up:
|
|
case Key.Down:
|
|
case Key.Left:
|
|
case Key.Right:
|
|
case Key.Escape:
|
|
case Key.Apps:
|
|
if (modifiers != ModifierKeys.None)
|
|
{
|
|
return;
|
|
}
|
|
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
CmdKeyProcessed?.Invoke(this, e.Key, modifiers);
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void AdjustDataGridViewHeight(Menu? menuPredecessor, double screenHeightMax)
|
|
{
|
|
double factor = Settings.Default.RowHeighteInPercentage / 100f;
|
|
if (NativeMethods.IsTouchEnabled())
|
|
{
|
|
factor = Settings.Default.RowHeighteInPercentageTouch / 100f;
|
|
}
|
|
|
|
if (menuPredecessor == null)
|
|
{
|
|
if (dgv.Tag == null && dgv.Items.Count > 0)
|
|
{
|
|
// dgv.AutoResizeRows(); slightly incorrect depending on dpi
|
|
// 100% = 20 instead 21
|
|
// 125% = 23 instead 27, 150% = 28 instead 32
|
|
// 175% = 33 instead 37, 200% = 35 instead 42
|
|
// #418 use 21 as default and scale it manually
|
|
double rowHeightDefault = 21.24d * Scaling.FactorByDpi;
|
|
Resources["RowHeight"] = Math.Round(rowHeightDefault * factor * Scaling.Factor);
|
|
dgv.Tag = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Take over the height from predecessor menu
|
|
Resources["RowHeight"] = menuPredecessor.Resources["RowHeight"];
|
|
dgv.Tag = true;
|
|
}
|
|
|
|
double heightMaxByOptions = Scaling.Factor * Scaling.FactorByDpi *
|
|
450f * (Settings.Default.HeightMaxInPercent / 100f);
|
|
MaxHeight = Math.Min(screenHeightMax, heightMaxByOptions);
|
|
}
|
|
|
|
private void AdjustDataGridViewWidth()
|
|
{
|
|
if (!string.IsNullOrEmpty(textBoxSearch.Text))
|
|
{
|
|
return;
|
|
}
|
|
|
|
double factorIconSizeInPercent = Settings.Default.IconSizeInPercent / 100f;
|
|
|
|
// IcoWidth 100% = 21px, 175% is 33
|
|
double icoWidth = 16 * Scaling.FactorByDpi;
|
|
Resources["ColumnIconWidth"] = Math.Ceiling(icoWidth * factorIconSizeInPercent * Scaling.Factor);
|
|
|
|
double renderedMaxWidth = 0D;
|
|
foreach (RowData item in dgv.Items)
|
|
{
|
|
double renderedWidth = new FormattedText(
|
|
item.ColumnText,
|
|
CultureInfo.CurrentCulture,
|
|
FlowDirection.LeftToRight,
|
|
new Typeface(dgv.FontFamily, dgv.FontStyle, dgv.FontWeight, dgv.FontStretch),
|
|
dgv.FontSize,
|
|
dgv.Foreground,
|
|
VisualTreeHelper.GetDpi(this).PixelsPerDip).Width;
|
|
if (renderedWidth > renderedMaxWidth)
|
|
{
|
|
renderedMaxWidth = renderedWidth;
|
|
}
|
|
}
|
|
|
|
Resources["ColumnTextWidth"] = Math.Ceiling(Math.Min(
|
|
renderedMaxWidth,
|
|
(double)(Scaling.Factor * Scaling.FactorByDpi * 400f * (Settings.Default.WidthMaxInPercent / 100f))));
|
|
}
|
|
|
|
private void HandleScrollChanged(object sender, ScrollChangedEventArgs e)
|
|
{
|
|
if (IsLoaded)
|
|
{
|
|
MenuScrolled?.Invoke();
|
|
}
|
|
}
|
|
|
|
private void TextBoxSearch_TextChanged(bool causedByWatcherUpdate)
|
|
{
|
|
SearchTextChanging?.Invoke();
|
|
|
|
string? userPattern = textBoxSearch.Text?.Replace("%", " ").Replace("*", " ").ToLower();
|
|
|
|
CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(dgv.ItemsSource);
|
|
if (string.IsNullOrEmpty(userPattern))
|
|
{
|
|
SizeToContent = SizeToContent.WidthAndHeight;
|
|
view.Filter = (object item) => Filter_Default((RowData)item);
|
|
}
|
|
else
|
|
{
|
|
SizeToContent = SizeToContent.Manual;
|
|
view.Filter = (object item) => Filter_ByUserPattern((RowData)item, userPattern);
|
|
}
|
|
|
|
SearchTextChanged?.Invoke(this, string.IsNullOrEmpty(userPattern), causedByWatcherUpdate);
|
|
}
|
|
|
|
private void PictureBoxOpenFolder_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
Menus.OpenFolder(folderPath);
|
|
}
|
|
|
|
private void PictureBoxMenuAlwaysOpen_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (Config.AlwaysOpenByPin = !Config.AlwaysOpenByPin)
|
|
{
|
|
pictureBoxMenuAlwaysOpen.Source = (DrawingImage)Resources["ic_fluent_pin_48_filledDrawingImage"];
|
|
}
|
|
else
|
|
{
|
|
pictureBoxMenuAlwaysOpen.Source = (DrawingImage)Resources["ic_fluent_pin_48_regularDrawingImage"];
|
|
}
|
|
}
|
|
|
|
private void PictureBoxSettings_MouseClick(object sender, RoutedEventArgs e)
|
|
{
|
|
SettingsWindow.ShowSingleInstance();
|
|
}
|
|
|
|
private void PictureBoxRestart_MouseClick(object sender, RoutedEventArgs e)
|
|
{
|
|
AppRestart.ByMenuButton();
|
|
}
|
|
|
|
private void MainMenu_MoveStart(object sender, MouseButtonEventArgs e)
|
|
{
|
|
// Hide all sub menus to clear the view for repositioning of the main menu
|
|
if (SubMenu != null)
|
|
{
|
|
SubMenu?.HideWithFade(true);
|
|
RefreshSelection();
|
|
}
|
|
|
|
lastLocation = NativeMethods.Screen.CursorPosition;
|
|
MouseMove += MainMenu_MoveRelocate;
|
|
MouseUp += MainMenu_MoveEnd;
|
|
Deactivated += MainMenu_MoveEnd;
|
|
Mouse.Capture(this);
|
|
}
|
|
|
|
private void MainMenu_MoveRelocate(object sender, MouseEventArgs e)
|
|
{
|
|
Point mousePos = NativeMethods.Screen.CursorPosition;
|
|
Left = Left + mousePos.X - lastLocation.X;
|
|
Top = Top + mousePos.Y - lastLocation.Y;
|
|
lastLocation = mousePos;
|
|
|
|
Settings.Default.CustomLocationX = (int)Left;
|
|
Settings.Default.CustomLocationY = (int)Top;
|
|
}
|
|
|
|
private void MainMenu_MoveEnd(object? sender, EventArgs? e)
|
|
{
|
|
Mouse.Capture(null);
|
|
MouseMove -= MainMenu_MoveRelocate;
|
|
MouseUp -= MainMenu_MoveEnd;
|
|
Deactivated -= MainMenu_MoveEnd;
|
|
|
|
if (Settings.Default.UseCustomLocation)
|
|
{
|
|
if (!SettingsWindow.IsOpen())
|
|
{
|
|
Settings.Default.Save();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs? e)
|
|
{
|
|
if (e != null)
|
|
{
|
|
foreach (RowData itemData in e.AddedItems)
|
|
{
|
|
itemData.IsSelected = true;
|
|
itemData.UpdateColors();
|
|
}
|
|
|
|
foreach (RowData itemData in e.RemovedItems)
|
|
{
|
|
itemData.IsSelected = false;
|
|
itemData.UpdateColors();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// TODO: Refactor item selection to prevent running this loop
|
|
ListView lv = (ListView)sender;
|
|
foreach (RowData itemData in lv.Items.SourceCollection)
|
|
{
|
|
itemData.IsSelected = lv.SelectedItem == itemData;
|
|
itemData.UpdateColors();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ListView_MouseLeave(object sender, MouseEventArgs e)
|
|
{
|
|
// In case a sub menu is already open and another item was already selected
|
|
// but WaitToLoadMenu hasn't take action yet
|
|
// then we want to reset that selection, so the sub menu selection remains active only
|
|
if (SubMenu != null)
|
|
{
|
|
ListView lv = (ListView)sender;
|
|
foreach (RowData itemData in lv.Items)
|
|
{
|
|
if (itemData.SubMenu == SubMenu)
|
|
{
|
|
lv.SelectedItem = itemData;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ListView_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
int count = dgv.Items.Count;
|
|
labelStatus.Content = count.ToString() + " " + Translator.GetText(count == 1 ? "element" : "elements");
|
|
}
|
|
|
|
private void ListViewItem_MouseEnter(object sender, MouseEventArgs e)
|
|
{
|
|
if (!isShellContextMenuOpen)
|
|
{
|
|
CellMouseEnter?.Invoke((RowData)((ListViewItem)sender).Content);
|
|
}
|
|
}
|
|
|
|
private void ListViewItem_MouseLeave(object sender, MouseEventArgs e)
|
|
{
|
|
if (!isShellContextMenuOpen)
|
|
{
|
|
CellMouseLeave?.Invoke();
|
|
}
|
|
}
|
|
|
|
private void ListViewItem_PreviewMouseDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
RowData itemData = (RowData)((ListViewItem)sender).Content;
|
|
|
|
CellMouseDown?.Invoke(itemData);
|
|
|
|
if (e.RightButton == MouseButtonState.Pressed)
|
|
{
|
|
// Somehow when the TrackPopupMenuEx is called to spawn the context menu
|
|
// This will fire a MouseEnter event on the next item, like WTF?!
|
|
// Prevent any use of MouseEnter/MouseLeave while the menu is open.
|
|
// TODO: Find root case and fix this properly.
|
|
isShellContextMenuOpen = true;
|
|
var position = Mouse.GetPosition(this);
|
|
position.Offset(Left, Top);
|
|
itemData.OpenShellContextMenu(position);
|
|
isShellContextMenuOpen = false;
|
|
}
|
|
}
|
|
|
|
private void ListViewItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) =>
|
|
((RowData)((ListViewItem)sender).Content).OpenItem(e.ClickCount);
|
|
}
|
|
}
|