diff --git a/.gitignore b/.gitignore index b02ee01..cd937ea 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ obj/ # Artifacts artifacts/ +src/.idea/ +*.user +desktop.ini \ No newline at end of file diff --git a/README.md b/README.md index 0388b3d..a705e15 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,58 @@ # FlaUInspect + ![FlaUInspect](/FlaUInspect.png?raw=true) -### New 2.0.0 -New has been [released](https://github.com/FlaUI/FlaUInspect/releases/tag/v2.0.0)! +## NEW 3.0.0 + +New version released! Download it [here](https://github.com/FlaUI/FlaUInspect/releases/tag/v3.0.0) This is a major update with a lot of changes: +- using separated window for process +- new UI +- Dark and Light theme support +- implement settings +- new icons +- new application selection bounding +- redesigned: + - property grid + - control tree + - menu buttons + - mouse hover control selection +- improved performance +- fixed and redesigned 3-state highlight of selected control + +#### Screenshot of new UI: + +Main window and selection application highlights. + +Image + +Main window display application and allow user to select it from list or press and hold Find window button and drag mouse over applications. The FlaUinspect will highlight the application under mouse cursor and select id in the list. + +Hover mode + +Image + +Select Hover mode button and hold Ctrl key and move mouse over application windows. The FlaUInspect will highlight the control under mouse cursor and select it in the tree. + +Highlight selection + +Image + +Select Selection Mode button and click on any control in the application. The FlaUInspect will highlight the selected control in the application. + +Highlight selectin and Dark theme + +Image + +FlaUinspect supports Light and Dark theme currently. + +### 2.0.0 + +Download it [here](https://github.com/FlaUI/FlaUInspect/releases/tag/v2.0.0) + +This is a major update with a lot of changes: + * Complete rewrite of the application * New UI * New features @@ -12,25 +60,30 @@ This is a major update with a lot of changes: * Based on .NET 8 * Three separate versions for UIA2 and UIA3 and default with choosing on startup - ### Installation -To install FlaUInspect, either build it yourself or get the zip from the releases page here on GitHub (https://github.com/FlaUI/FlaUInspect/releases). + +To install FlaUInspect, either build it yourself or get the zip from the releases page here on +GitHub (https://github.com/FlaUI/FlaUInspect/releases). ### Description -There are various tools around which help inspecting application that should be ui tested or automated. Some of them are: + +There are various tools around which help inspecting application that should be ui tested or automated. Some of them +are: + * VisualUIAVerify * Inspect * UISpy * and probably others -Most of them are old and sometimes not very stable and (if open source), a code mess to maintain. + Most of them are old and sometimes not very stable and (if open source), a code mess to maintain. FlaUInspect is supposed to be a modern alternative, based on [FlaUI](https://github.com/Roemer/FlaUI). -On startup of FlaUInspect, you can choose if you want to use UIA2 or UIA3 (see [FAQ](https://github.com/Roemer/FlaUI/wiki/FAQ) why you can't use both at the same time). +On startup of FlaUInspect, you can choose if you want to use UIA2 or UIA3 ( +see [FAQ](https://github.com/Roemer/FlaUI/wiki/FAQ) why you can't use both at the same time). You can use pre-built versions of FlaUInspect.UIA2 and FlaUInspect.UIA3 if you want to use a version of UIA. - ###### Main Screen + ![Main Screen](https://github.com/user-attachments/assets/6212341b-9776-4907-9edc-acc00073c92e) ##### Tool buttons @@ -43,10 +96,11 @@ You can use pre-built versions of FlaUInspect.UIA2 and FlaUInspect.UIA3 if you w | Show XPath | Enable this option to show a simple XPath to the current selected element in the StatusBar of FlaUInspect | ### Release Notes 2.0.0 + * Deployed FlaUInspect, FlaUInspect.UIA2, and FlaUInspect.UIA3 applications with hardcoded UI2 or UI3 selection. * Refactored code and implemented asynchronous operations. * Redesigned the property grid; collapsed groups remain collapsed after selecting another control. * Added a third selection state: highlighting the selected control in an application. * Included a refresh button for each item in the tree. -*Refined icons. + *Refined icons. diff --git a/src/FlaUInspect/App.xaml b/src/FlaUInspect/App.xaml index 3ab6a2d..0cb2912 100644 --- a/src/FlaUInspect/App.xaml +++ b/src/FlaUInspect/App.xaml @@ -1,6 +1,11 @@  - + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + + + + + + + \ No newline at end of file diff --git a/src/FlaUInspect/App.xaml.cs b/src/FlaUInspect/App.xaml.cs index 858961d..3137dcb 100644 --- a/src/FlaUInspect/App.xaml.cs +++ b/src/FlaUInspect/App.xaml.cs @@ -1,20 +1,57 @@ -using System.Reflection; +using System.Drawing; +using System.IO; +using System.Reflection; using System.Windows; -using FlaUI.Core; +using FlaUInspect.Core; using FlaUInspect.Core.Logger; +using FlaUInspect.Settings; using FlaUInspect.ViewModels; using FlaUInspect.Views; +using Microsoft.Extensions.DependencyInjection; namespace FlaUInspect; public partial class App { - private void ApplicationStart(object sender, StartupEventArgs e) { + + public static IServiceProvider Services { get; private set; } = null!; + public static FlaUiAppOptions FlaUiAppOptions { get; } = new (); + + public static InternalLogger Logger { get; } = new (); + + protected override void OnStartup(StartupEventArgs e) { + base.OnStartup(e); + + ServiceCollection services = new (); + services.AddSingleton>(_ => new JsonSettingsService(Path.Combine(AppContext.BaseDirectory, $"appsettings.json"))); + Services = services.BuildServiceProvider(); + + ISettingsService settingsService = Services.GetRequiredService>(); + FlaUiAppSettings flaUiAppSettings = settingsService.Load(); + ApplyAppOption(flaUiAppSettings); + + //InternalLogger logger = new (); + Current.ShutdownMode = ShutdownMode.OnMainWindowClose; + StartupViewModel startupViewModel = new (); + StartupWindow startupWindow = new (Logger) { DataContext = startupViewModel }; + Current.MainWindow = startupWindow; + startupWindow.Show(); + + //Preload light theme + SetTheme(flaUiAppSettings); + + Task.Run(() => startupViewModel.Init()); + + return; AssemblyFileVersionAttribute? versionAttribute = Assembly.GetEntryAssembly()?.GetCustomAttribute(typeof(AssemblyFileVersionAttribute)) as AssemblyFileVersionAttribute; string applicationVersion = versionAttribute?.Version ?? "N/A"; - InternalLogger logger = new (); + string windowHandle = string.Empty; + + if (e.Args.Length > 0) { + windowHandle = e.Args[0]; + } #if AUTOMATION_UIA3 - MainViewModel mainViewModel = new (AutomationType.UIA3, applicationVersion, logger); + MainViewModel mainViewModel = new (AutomationType.UIA3, applicationVersion, windowHandle, logger); MainWindow mainWindow = new () { DataContext = mainViewModel }; //Re-enable normal shutdown mode. @@ -22,7 +59,7 @@ private void ApplicationStart(object sender, StartupEventArgs e) { Current.MainWindow = mainWindow; mainWindow.Show(); #elif AUTOMATION_UIA2 - MainViewModel mainViewModel = new (AutomationType.UIA2, applicationVersion, logger); + MainViewModel mainViewModel = new (AutomationType.UIA2, applicationVersion, windowHandle, logger); MainWindow mainWindow = new() { DataContext = mainViewModel }; //Re-enable normal shutdown mode. @@ -30,19 +67,87 @@ private void ApplicationStart(object sender, StartupEventArgs e) { Current.MainWindow = mainWindow; mainWindow.Show(); #else - Current.ShutdownMode = ShutdownMode.OnExplicitShutdown; - ChooseVersionWindow dialog = new (); + // Current.ShutdownMode = ShutdownMode.OnExplicitShutdown; + // ChooseVersionWindow dialog = new (); + // + // if (dialog.ShowDialog() == true) { + // + // MainViewModel mainViewModel = new (dialog.SelectedAutomationType, applicationVersion, windowHandle, Logger); + // MainWindow mainWindow = new () { DataContext = mainViewModel }; + // + // //Re-enable normal shutdown mode. + // Current.ShutdownMode = ShutdownMode.OnMainWindowClose; + // Current.MainWindow = mainWindow; + // mainWindow.Show(); + // } +#endif + } - if (dialog.ShowDialog() == true) { + public static void ApplyAppOption(FlaUiAppSettings settings) { + // Apply theme + Current.Dispatcher.Invoke(() => { + SetTheme(settings); + }); - MainViewModel mainViewModel = new (dialog.SelectedAutomationType, applicationVersion, logger); - MainWindow mainWindow = new () { DataContext = mainViewModel }; + ThicknessConverter converter = new (); + FlaUiAppSettings cloneSetting = settings.Clone() as FlaUiAppSettings; - //Re-enable normal shutdown mode. - Current.ShutdownMode = ShutdownMode.OnMainWindowClose; - Current.MainWindow = mainWindow; - mainWindow.Show(); + if (settings.HoverOverlay != null) { + Thickness hoverMargin = (Thickness)converter.ConvertFromString(cloneSetting.HoverOverlay.Margin); + FlaUiAppOptions.HoverOverlay = () => new ElementOverlay( + new ElementOverlayConfiguration(cloneSetting.HoverOverlay.Size, + hoverMargin, + ColorTranslator.FromHtml(cloneSetting.HoverOverlay.OverlayColor), + ElementOverlay.GetRectangleFactory(cloneSetting.HoverOverlay.OverlayMode))); + } else { + FlaUiAppOptions.HoverOverlay = FlaUiAppOptions.DefaultOverlay; } -#endif + + if (settings.SelectionOverlay != null) { + Thickness selectionMargin = (Thickness)converter.ConvertFromString(cloneSetting.SelectionOverlay.Margin); + FlaUiAppOptions.SelectionOverlay = () => new ElementOverlay( + new ElementOverlayConfiguration(cloneSetting.SelectionOverlay.Size, + selectionMargin, + ColorTranslator.FromHtml(cloneSetting.SelectionOverlay.OverlayColor), + ElementOverlay.GetRectangleFactory(cloneSetting.SelectionOverlay.OverlayMode))); + } else { + FlaUiAppOptions.SelectionOverlay = FlaUiAppOptions.DefaultOverlay; + } + + if (settings.PickOverlay != null) { + Thickness pickMargin = (Thickness)converter.ConvertFromString(cloneSetting.PickOverlay.Margin); + FlaUiAppOptions.PickOverlay = () => new ElementOverlay( + new ElementOverlayConfiguration(cloneSetting.PickOverlay.Size, + pickMargin, + ColorTranslator.FromHtml(cloneSetting.PickOverlay.OverlayColor), + ElementOverlay.GetRectangleFactory(cloneSetting.PickOverlay.OverlayMode))); + } else { + FlaUiAppOptions.PickOverlay = FlaUiAppOptions.DefaultOverlay; + } + } + + private static void SetTheme(FlaUiAppSettings settings) { + ResourceDictionary newTheme = new(); + + switch (settings.Theme) { + case "Dark": + newTheme.Source = new Uri("/FlaUInspect;component/Themes/DarkTheme.xaml", UriKind.Relative); + break; + default: + newTheme.Source = new Uri("/FlaUInspect;component/Themes/LightTheme.xaml", UriKind.Relative); + break; + } + + // Remove existing theme dictionaries + for (int i = Current.Resources.MergedDictionaries.Count - 1; i >= 0; i--) { + ResourceDictionary dict = Current.Resources.MergedDictionaries[i]; + + if (dict.Source != null && (dict.Source.OriginalString.Contains("Themes/DarkTheme.xaml") || dict.Source.OriginalString.Contains("Themes/LightTheme.xaml"))) { + Current.Resources.MergedDictionaries.RemoveAt(i); + } + } + + // Add the new theme dictionary + Current.Resources.MergedDictionaries.Add(newTheme); } } \ No newline at end of file diff --git a/src/FlaUInspect/Controls/Buttons.xaml b/src/FlaUInspect/Controls/Buttons.xaml new file mode 100644 index 0000000..46c0c4d --- /dev/null +++ b/src/FlaUInspect/Controls/Buttons.xaml @@ -0,0 +1,38 @@ + + + \ No newline at end of file diff --git a/src/FlaUInspect/Controls/CheckBoxes.xaml b/src/FlaUInspect/Controls/CheckBoxes.xaml new file mode 100644 index 0000000..d88fd70 --- /dev/null +++ b/src/FlaUInspect/Controls/CheckBoxes.xaml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/FlaUInspect/Resources/ChooseVersionIcons.xaml b/src/FlaUInspect/Controls/ChooseVersionIcons.xaml similarity index 99% rename from src/FlaUInspect/Resources/ChooseVersionIcons.xaml rename to src/FlaUInspect/Controls/ChooseVersionIcons.xaml index afed77b..46effec 100644 --- a/src/FlaUInspect/Resources/ChooseVersionIcons.xaml +++ b/src/FlaUInspect/Controls/ChooseVersionIcons.xaml @@ -9,7 +9,7 @@ - + @@ -26,7 +26,7 @@ - + diff --git a/src/FlaUInspect/Controls/Comboboxes.xaml b/src/FlaUInspect/Controls/Comboboxes.xaml new file mode 100644 index 0000000..1912839 --- /dev/null +++ b/src/FlaUInspect/Controls/Comboboxes.xaml @@ -0,0 +1,285 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/FlaUInspect/Controls/ElementToggleExpander.xaml b/src/FlaUInspect/Controls/ElementToggleExpander.xaml new file mode 100644 index 0000000..cc3480e --- /dev/null +++ b/src/FlaUInspect/Controls/ElementToggleExpander.xaml @@ -0,0 +1,38 @@ + + + + + + + + \ No newline at end of file diff --git a/src/FlaUInspect/Controls/GroupStyle.xaml b/src/FlaUInspect/Controls/GroupStyle.xaml new file mode 100644 index 0000000..de52b52 --- /dev/null +++ b/src/FlaUInspect/Controls/GroupStyle.xaml @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/src/FlaUInspect/Styles/ImageStyles.xaml b/src/FlaUInspect/Controls/ImageStyles.xaml similarity index 99% rename from src/FlaUInspect/Styles/ImageStyles.xaml rename to src/FlaUInspect/Controls/ImageStyles.xaml index a19cae5..7cb733e 100644 --- a/src/FlaUInspect/Styles/ImageStyles.xaml +++ b/src/FlaUInspect/Controls/ImageStyles.xaml @@ -201,6 +201,7 @@ + + \ No newline at end of file diff --git a/src/FlaUInspect/Controls/ListViews.xaml b/src/FlaUInspect/Controls/ListViews.xaml new file mode 100644 index 0000000..509d084 --- /dev/null +++ b/src/FlaUInspect/Controls/ListViews.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/FlaUInspect/Controls/PatternExpanderStyle.xaml b/src/FlaUInspect/Controls/PatternExpanderStyle.xaml new file mode 100644 index 0000000..83c0f2f --- /dev/null +++ b/src/FlaUInspect/Controls/PatternExpanderStyle.xaml @@ -0,0 +1,47 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/FlaUInspect/Controls/RadioButtons.xaml b/src/FlaUInspect/Controls/RadioButtons.xaml new file mode 100644 index 0000000..e5f8353 --- /dev/null +++ b/src/FlaUInspect/Controls/RadioButtons.xaml @@ -0,0 +1,59 @@ + + + + + \ No newline at end of file diff --git a/src/FlaUInspect/Controls/ScrollableHeaderedControl.xaml b/src/FlaUInspect/Controls/ScrollableHeaderedControl.xaml new file mode 100644 index 0000000..1f6eff1 --- /dev/null +++ b/src/FlaUInspect/Controls/ScrollableHeaderedControl.xaml @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/src/FlaUInspect/Controls/TextBlocks.xaml b/src/FlaUInspect/Controls/TextBlocks.xaml new file mode 100644 index 0000000..585fa8d --- /dev/null +++ b/src/FlaUInspect/Controls/TextBlocks.xaml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/src/FlaUInspect/Controls/TextBoxes.xaml b/src/FlaUInspect/Controls/TextBoxes.xaml new file mode 100644 index 0000000..47a1875 --- /dev/null +++ b/src/FlaUInspect/Controls/TextBoxes.xaml @@ -0,0 +1,54 @@ + + + + \ No newline at end of file diff --git a/src/FlaUInspect/Controls/ToggleButtons.xaml b/src/FlaUInspect/Controls/ToggleButtons.xaml new file mode 100644 index 0000000..f105312 --- /dev/null +++ b/src/FlaUInspect/Controls/ToggleButtons.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/FlaUInspect/Core/Behaviors/TreeViewBringIntoViewBehavior.cs b/src/FlaUInspect/Core/Behaviors/TreeViewBringIntoViewBehavior.cs new file mode 100644 index 0000000..71e8a45 --- /dev/null +++ b/src/FlaUInspect/Core/Behaviors/TreeViewBringIntoViewBehavior.cs @@ -0,0 +1,70 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Threading; + +namespace FlaUInspect.Core.Behaviors; + +public static class TreeViewBringIntoViewBehavior { + public static readonly DependencyProperty BringSelectedItemIntoViewProperty = + DependencyProperty.RegisterAttached( + "BringSelectedItemIntoView", + typeof(bool), + typeof(TreeViewBringIntoViewBehavior), + new PropertyMetadata(false, OnChanged)); + + public static bool GetBringSelectedItemIntoView(DependencyObject obj) { + return (bool)obj.GetValue(BringSelectedItemIntoViewProperty); + } + + public static void SetBringSelectedItemIntoView(DependencyObject obj, bool value) { + obj.SetValue(BringSelectedItemIntoViewProperty, value); + } + + private static void OnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { + if (d is not TreeView treeView) { + return; + } + + if ((bool)e.NewValue) { + treeView.SelectedItemChanged += TreeView_SelectedItemChanged; + } else { + treeView.SelectedItemChanged -= TreeView_SelectedItemChanged; + } + } + + private static void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs e) { + if (sender is not TreeView treeView || e.NewValue == null) { + return; + } + + treeView.Dispatcher.BeginInvoke(new Action(() => { + TreeViewItem? item = GetTreeViewItem(treeView, e.NewValue); + item?.BringIntoView(); + }), + DispatcherPriority.Background); + } + + private static TreeViewItem? GetTreeViewItem(ItemsControl container, object item) { + if (container.DataContext == item) + return container as TreeViewItem; + + foreach (object? i in container.Items) { + ItemsControl? child = container.ItemContainerGenerator.ContainerFromItem(i) as ItemsControl; + + if (child == null) + continue; + + if (child is TreeViewItem tvi && !tvi.IsExpanded) { + tvi.IsExpanded = true; + } + + TreeViewItem? result = GetTreeViewItem(child, item); + + if (result != null) { + return result; + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/Converters/IndentConverter.cs b/src/FlaUInspect/Core/Converters/IndentConverter.cs new file mode 100644 index 0000000..fe2bd5d --- /dev/null +++ b/src/FlaUInspect/Core/Converters/IndentConverter.cs @@ -0,0 +1,16 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace FlaUInspect.Core.Converters; + +public class IndentConverter : IValueConverter { + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { + return new Thickness((int)value * 16, 0, 0, 0); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/Converters/IntToVisibilityConverter.cs b/src/FlaUInspect/Core/Converters/IntToVisibilityConverter.cs new file mode 100644 index 0000000..cb803ce --- /dev/null +++ b/src/FlaUInspect/Core/Converters/IntToVisibilityConverter.cs @@ -0,0 +1,23 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using NotSupportedException = FlaUI.Core.Exceptions.NotSupportedException; + +namespace FlaUInspect.Core.Converters; + +public class IntToVisibilityConverter : IValueConverter { + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { + if (value is int intValue) { + if (parameter is "EqualsZero") { + return intValue == 0 ? Visibility.Visible : Visibility.Collapsed; + } else if (parameter is "GreaterThanZero") { + return intValue > 0 ? Visibility.Visible : Visibility.Collapsed; + } + } + return Visibility.Collapsed; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/Converters/InvertBooleanConverter.cs b/src/FlaUInspect/Core/Converters/InvertBooleanConverter.cs new file mode 100644 index 0000000..1215aab --- /dev/null +++ b/src/FlaUInspect/Core/Converters/InvertBooleanConverter.cs @@ -0,0 +1,18 @@ +using System.Globalization; +using System.Windows.Data; +using FlaUI.Core.Exceptions; + +namespace FlaUInspect.Core.Converters; + +public class InvertBooleanConverter : IValueConverter { + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { + if (value is bool boolValue) { + return !boolValue; + } + throw new ArgumentException(@"Value must be a boolean", nameof(value)); + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { + throw new NotSupportedByFrameworkException(); + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/Converters/IsStringNullOrEmptyToVisibleConverter.cs b/src/FlaUInspect/Core/Converters/IsStringNullOrEmptyToVisibleConverter.cs new file mode 100644 index 0000000..d64d8df --- /dev/null +++ b/src/FlaUInspect/Core/Converters/IsStringNullOrEmptyToVisibleConverter.cs @@ -0,0 +1,21 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace FlaUInspect.Core.Converters; + +public class IsStringNullOrEmptyToVisibleConverter : IValueConverter { + public bool IsInverted { get; set; } + + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { + var str = value as string; + bool condition = string.IsNullOrEmpty(str); + condition = IsInverted ? !condition : condition; + + return condition ? Visibility.Collapsed : Visibility.Visible; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/Converters/NullToVisibilityConverter.cs b/src/FlaUInspect/Core/Converters/NullToVisibilityConverter.cs new file mode 100644 index 0000000..9c593b0 --- /dev/null +++ b/src/FlaUInspect/Core/Converters/NullToVisibilityConverter.cs @@ -0,0 +1,15 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace FlaUInspect.Core.Converters; + +public class NullToVisibilityConverter : IValueConverter { + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { + return value == null ? Visibility.Collapsed : Visibility.Visible; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/Converters/StringToThicknessConverter.cs b/src/FlaUInspect/Core/Converters/StringToThicknessConverter.cs new file mode 100644 index 0000000..a1b25a1 --- /dev/null +++ b/src/FlaUInspect/Core/Converters/StringToThicknessConverter.cs @@ -0,0 +1,35 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace FlaUInspect.Core.Converters; + +public class StringToThicknessConverter : IValueConverter { + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { + if (value is string str && !string.IsNullOrWhiteSpace(str)) { + string[] parts = str.Split(new char[] { ',', ' ' }); + + if (parts.Length == 4 && + double.TryParse(parts[0], out double left) && + double.TryParse(parts[1], out double top) && + double.TryParse(parts[2], out double right) && + double.TryParse(parts[3], out double bottom)) { + return new Thickness(left, top, right, bottom); + } + + if (parts.Length == 2 && + double.TryParse(parts[0], out double leftRigth) && + double.TryParse(parts[1], out double topBottom)) { + return new Thickness(leftRigth, topBottom, leftRigth, topBottom); + } + } + return new Thickness(0); + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { + if (value is Thickness thickness) { + return $"{thickness.Left},{thickness.Top},{thickness.Right},{thickness.Bottom}"; + } + return string.Empty; + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/Editable.cs b/src/FlaUInspect/Core/Editable.cs new file mode 100644 index 0000000..876a81e --- /dev/null +++ b/src/FlaUInspect/Core/Editable.cs @@ -0,0 +1,45 @@ +using System.Reflection; + +namespace FlaUInspect.Core; + +public class Editable : ObservableObject where T : class, new() { + private readonly Action _apply; + + private readonly Func _clone; + private readonly Func _equals; + + public Editable(T original, Func clone, Action apply, Func equals) { + Original = original; + _clone = clone; + _apply = apply; + _equals = equals; + + Current = clone(original); + } + + public T Original { get; } + public T Current { get; } + + public bool IsDirty => !_equals(Current, Original); + + public void Apply(object? obj) { + _apply(Current, Original); + RaiseDirty(); + } + + public void Reset(object? obj) { + Copy(_clone(Original), Current); + RaiseDirty(); + } + + private void RaiseDirty() { + OnPropertyChanged(nameof(IsDirty)); + } + + private static void Copy(T from, T to) { + foreach (PropertyInfo p in typeof(T).GetProperties() + .Where(p => p.CanRead && p.CanWrite)) { + p.SetValue(to, p.GetValue(from)); + } + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/ElementHighlighter.cs b/src/FlaUInspect/Core/ElementHighlighter.cs deleted file mode 100644 index c1a22a6..0000000 --- a/src/FlaUInspect/Core/ElementHighlighter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Drawing; -using FlaUI.Core.AutomationElements; -using FlaUI.Core.Exceptions; -using FlaUInspect.Core.Logger; - -namespace FlaUInspect.Core; - -public static class ElementHighlighter { - public static void HighlightElement(AutomationElement? automationElement, ILogger? logger) { - try { - Task.Run(() => automationElement?.DrawHighlight(false, Color.Red, TimeSpan.FromSeconds(1))); - } catch (PropertyNotSupportedException ex) { - logger?.LogError($"Exception: {ex.Message}"); - } - } -} \ No newline at end of file diff --git a/src/FlaUInspect/Core/ElementOverlay.cs b/src/FlaUInspect/Core/ElementOverlay.cs new file mode 100644 index 0000000..ea30ccf --- /dev/null +++ b/src/FlaUInspect/Core/ElementOverlay.cs @@ -0,0 +1,92 @@ +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows; +using FlaUI.Core.Overlay; + +namespace FlaUInspect.Core; + +public class ElementOverlay : IDisposable { + + private OverlayRectangleForm[] _overlayRectangleFormList = []; + + public ElementOverlay(ElementOverlayConfiguration configuration) { + Configuration = configuration; + + } + + public ElementOverlayConfiguration Configuration { get; } + + public void Dispose() { + Hide(); + } + + public static Func GetRectangleFactory(string mode) { + return mode.ToLower() switch { + "fill" => FillRectangleFactory, + "border" => BoundRectangleFactory, + _ => BoundRectangleFactory + }; + } + + public static Rectangle[] FillRectangleFactory(ElementOverlayConfiguration config, Rectangle rectangle) { + return [ + new Rectangle(rectangle.X - (int)config.Margin.Left, + rectangle.Y - (int)config.Margin.Top, + rectangle.Width + (int)config.Margin.Right, + rectangle.Height + (int)config.Margin.Bottom) + ]; + } + + public static Rectangle[] BoundRectangleFactory(ElementOverlayConfiguration config, Rectangle rectangle) { + return [ + new Rectangle(rectangle.X - (int)config.Margin.Left, rectangle.Y - (int)config.Margin.Top, config.Size, rectangle.Height + (int)config.Margin.Bottom), + new Rectangle(rectangle.X - (int)config.Margin.Left, rectangle.Y - (int)config.Margin.Top, rectangle.Width + (int)config.Margin.Right, config.Size), + new Rectangle(rectangle.X + rectangle.Width - config.Size + (int)config.Margin.Left, rectangle.Y - (int)config.Margin.Top, config.Size, rectangle.Height + (int)config.Margin.Bottom), + new Rectangle(rectangle.X - (int)config.Margin.Left, rectangle.Y + rectangle.Height - config.Size + (int)config.Margin.Right, rectangle.Width + (int)config.Margin.Right, config.Size) + ]; + } + + public void Hide() { + foreach (OverlayRectangleForm overlayRectangleForm in _overlayRectangleFormList) { + overlayRectangleForm.Hide(); + overlayRectangleForm.Close(); + overlayRectangleForm.Dispose(); + } + _overlayRectangleFormList = []; + } + + public void Show(Rectangle rectangle) { + Color color1 = Color.FromArgb(255, Configuration.Color.R, Configuration.Color.G, Configuration.Color.B); + Rectangle[] rectangles = Configuration.RectangleFactory?.Invoke(Configuration, rectangle) ?? BoundRectangleFactory(Configuration, rectangle); + + List rectangleForms = []; + + foreach (Rectangle rectangle1 in rectangles) { + OverlayRectangleForm overlayRectangleForm1 = new (); + overlayRectangleForm1.BackColor = color1; + overlayRectangleForm1.Opacity = Configuration.Color.A / 255d; + OverlayRectangleForm overlayRectangleForm2 = overlayRectangleForm1; + rectangleForms.Add(overlayRectangleForm2); + SetWindowPos(overlayRectangleForm2.Handle, new IntPtr(-1), rectangle1.X, rectangle1.Y, rectangle1.Width, rectangle1.Height, 16 /*0x10*/); + ShowWindow(overlayRectangleForm2.Handle, 8); + } + + _overlayRectangleFormList = rectangleForms.ToArray(); + } + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetWindowPos( + IntPtr hWnd, + IntPtr hwndAfter, + int x, + int y, + int width, + int height, + int flags); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); +} + +public record ElementOverlayConfiguration(int Size, Thickness Margin, Color Color, Func? RectangleFactory = null); \ No newline at end of file diff --git a/src/FlaUInspect/Core/Exporters/IElementDetailsExporter.cs b/src/FlaUInspect/Core/Exporters/IElementDetailsExporter.cs new file mode 100644 index 0000000..bb22b79 --- /dev/null +++ b/src/FlaUInspect/Core/Exporters/IElementDetailsExporter.cs @@ -0,0 +1,8 @@ +using FlaUInspect.Models; + +namespace FlaUInspect.Core.Exporters; + +public interface IElementDetailsExporter { + + string Export(IEnumerable automationElement); +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/Exporters/ITreeExporter.cs b/src/FlaUInspect/Core/Exporters/ITreeExporter.cs new file mode 100644 index 0000000..ed48e89 --- /dev/null +++ b/src/FlaUInspect/Core/Exporters/ITreeExporter.cs @@ -0,0 +1,7 @@ +using FlaUInspect.ViewModels; + +namespace FlaUInspect.Core.Exporters; + +public interface ITreeExporter { + string Export(ElementViewModel element); +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/Exporters/XmlElementDetailsExporter.cs b/src/FlaUInspect/Core/Exporters/XmlElementDetailsExporter.cs new file mode 100644 index 0000000..087865e --- /dev/null +++ b/src/FlaUInspect/Core/Exporters/XmlElementDetailsExporter.cs @@ -0,0 +1,24 @@ +using System.Xml.Linq; +using FlaUInspect.Models; +using FlaUInspect.ViewModels; + +namespace FlaUInspect.Core.Exporters; + +public class XmlElementDetailsExporter : IElementDetailsExporter { + public string Export(IEnumerable patternItems) { + XDocument document = new (); + XElement root = new ("Root"); + document.Add(root); + + foreach (ElementPatternItem elementPatternItem in patternItems.Where(x => x.IsVisible)) { + XElement xElement = new ("pattern", new XAttribute("Name", elementPatternItem.PatternName)); + + foreach (PatternItem patternItem in elementPatternItem.Children ?? []) { + xElement.Add(new XElement("property", new XAttribute("Name", patternItem.Key), new XAttribute("Value", patternItem.Value))); + } + root.Add(xElement); + } + + return document.ToString(); + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/Exporters/XmlTreeExporter.cs b/src/FlaUInspect/Core/Exporters/XmlTreeExporter.cs new file mode 100644 index 0000000..f21dd9e --- /dev/null +++ b/src/FlaUInspect/Core/Exporters/XmlTreeExporter.cs @@ -0,0 +1,47 @@ +using System.Xml.Linq; +using FlaUInspect.ViewModels; + +namespace FlaUInspect.Core.Exporters; + +public class XmlTreeExporter(bool enableXPath) : ITreeExporter { + + public string Export(ElementViewModel element) { + XDocument document = new (); + document.Add(new XElement("Root")); + ExportElement(document.Root!, element); + return document.ToString(); + } + + private void ExportElement(XElement parent, ElementViewModel element) { + XElement xElement = CreateXElement(element); + parent.Add(xElement); + + try { + foreach (ElementViewModel children in element.LoadChildren()) { + try { + ExportElement(xElement, children!); + } catch { + // ignored + } + } + } catch { + // ignored + } + } + + private XElement CreateXElement(ElementViewModel element) { + + List attrs = [ + new ("Name", element.Name), + new ("AutomationId", element.AutomationId), + new ("ControlType", element.ControlType) + ]; + + if (enableXPath) { + attrs.Add(new XAttribute("XPath", element.XPath)); + } + + XElement xElement = new ("Element", attrs); + return xElement; + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/FocusTrackingMode.cs b/src/FlaUInspect/Core/FocusTrackingMode.cs index ec2b0da..a1e3460 100644 --- a/src/FlaUInspect/Core/FocusTrackingMode.cs +++ b/src/FlaUInspect/Core/FocusTrackingMode.cs @@ -5,12 +5,10 @@ namespace FlaUInspect.Core; -public class FocusTrackingMode(AutomationBase? automation) { +public class FocusTrackingMode(AutomationBase? automation, Action onFocusChangedAction) { private AutomationElement? _currentFocusedElement; private FocusChangedEventHandlerBase? _eventHandler; - public event Action? ElementFocused; - public void Start() { // Might give problems because inspect is registered as well. // MS recommends to call UIA commands on a thread outside a UI thread. @@ -32,9 +30,12 @@ private void OnFocusChanged(AutomationElement? automationElement) { if (!Equals(_currentFocusedElement, automationElement)) { _currentFocusedElement = automationElement; - Application.Current.Dispatcher.Invoke(() => { - ElementFocused?.Invoke(automationElement); - }); + + if (automationElement != null) { + Application.Current.Dispatcher.Invoke(() => { + onFocusChangedAction(automationElement); + }); + } } } } \ No newline at end of file diff --git a/src/FlaUInspect/Core/GlobalMouseHook.cs b/src/FlaUInspect/Core/GlobalMouseHook.cs new file mode 100644 index 0000000..0425da3 --- /dev/null +++ b/src/FlaUInspect/Core/GlobalMouseHook.cs @@ -0,0 +1,67 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace FlaUInspect.Core; + +public static class GlobalMouseHook { + + private const int WH_MOUSE_LL = 14; + private const int WM_MOUSEMOVE = 0x0200; + private static IntPtr _hookId = IntPtr.Zero; + private static LowLevelMouseProc _proc = HookCallback; + + public static event Action? MouseMove; // your event + + public static void Start() { + _hookId = SetHook(_proc); + } + + public static void Stop() { + UnhookWindowsHookEx(_hookId); + } + + private static IntPtr SetHook(LowLevelMouseProc proc) { + using (Process curProcess = Process.GetCurrentProcess()) + using (ProcessModule curModule = curProcess.MainModule!) { + return SetWindowsHookEx(WH_MOUSE_LL, proc, GetModuleHandle(curModule.ModuleName), 0); + } + } + + private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) { + if (nCode >= 0 && wParam == (IntPtr)WM_MOUSEMOVE) { + MSLLHOOKSTRUCT data = Marshal.PtrToStructure(lParam); + MouseMove?.Invoke(data.pt.x, data.pt.y); + } + return CallNextHookEx(_hookId, nCode, wParam, lParam); + } + + [DllImport("user32.dll")] + private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, + IntPtr hMod, uint dwThreadId); + + [DllImport("user32.dll")] + private static extern bool UnhookWindowsHookEx(IntPtr hhk); + + [DllImport("user32.dll")] + private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr GetModuleHandle(string lpModuleName); + + private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam); + + [StructLayout(LayoutKind.Sequential)] + private struct MSLLHOOKSTRUCT { + public POINT pt; + public int mouseData; + public int flags; + public int time; + public IntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + private struct POINT { + public int x; + public int y; + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/HoverManager.cs b/src/FlaUInspect/Core/HoverManager.cs new file mode 100644 index 0000000..10524c9 --- /dev/null +++ b/src/FlaUInspect/Core/HoverManager.cs @@ -0,0 +1,114 @@ +using System.Windows.Input; +using System.Windows.Threading; +using FlaUI.Core; +using FlaUInspect.Core.Logger; +using AutomationElement = FlaUI.Core.AutomationElements.AutomationElement; +using Mouse = FlaUI.Core.Input.Mouse; +using Point = System.Drawing.Point; + +namespace FlaUInspect.Core; + +public static class HoverManager { + private static Func? _elementOverlayFunc; + private static ILogger? _logger; + private static AutomationBase? _automationBase; + private static AutomationElement? _hoveredElement; + private static ElementOverlay? _elementOverlay; + + private static readonly List>> Listeners = []; + + private static readonly HashSet EnabledListeners = []; + + private static readonly object LockObject = new (); + + static HoverManager() { + DispatcherTimer timer = new() { + Interval = TimeSpan.FromMilliseconds(300) + }; + timer.Tick += (s, e) => Refresh(); + timer.Start(); + } + + public static bool IsInitialized => _automationBase != null && _elementOverlayFunc != null; + + private static void Refresh() { + if (EnabledListeners.Count == 0) { + _elementOverlay?.Dispose(); + _hoveredElement = null; + return; + } + + if (Keyboard.Modifiers.HasFlag(ModifierKeys.Control)) { + Point screenPos = Mouse.Position; + + try { + AutomationElement? automationElement = _automationBase?.FromPoint(screenPos); + + if (automationElement == null || automationElement?.Properties.ProcessId == Environment.ProcessId) { + _elementOverlay?.Dispose(); + _hoveredElement = null; + return; + } + + if (automationElement != null && (_hoveredElement == null || !automationElement.Equals(_hoveredElement))) { + _elementOverlay?.Dispose(); + _hoveredElement = automationElement; + + foreach (KeyValuePair> keyValuePair in Listeners) { + try { + keyValuePair.Value?.Invoke(automationElement); + } catch { + // ignored + } + } + + try { + if (_elementOverlayFunc != null && EnabledListeners.Count > 0) { + ElementOverlay? elementOverlay = _elementOverlayFunc(); + elementOverlay?.Show(automationElement.Properties.BoundingRectangle.Value); + _elementOverlay = elementOverlay; + } + } catch { + // ignored + } + } + } catch { + // ignored + } + } + } + + public static void AddListener(IntPtr id, Action onElementHovered) { + lock (LockObject) { + Listeners.Add(new KeyValuePair>(id, onElementHovered)); + } + } + + public static void RemoveListener(IntPtr id) { + lock (LockObject) { + KeyValuePair>? pair = Listeners.FirstOrDefault(x => x.Key == id); + + if (pair != null) { + Listeners.Remove(pair.Value); + } + } + } + + public static void Enable(IntPtr intPtr) { + lock (LockObject) { + EnabledListeners.Add(intPtr); + } + } + + public static void Disable(IntPtr intPtr) { + lock (LockObject) { + EnabledListeners.Remove(intPtr); + } + } + + public static void Initialize(AutomationBase? automation, Func elementOverlayFunc, ILogger? logger) { + _automationBase = automation; + _logger = logger; + _elementOverlayFunc = elementOverlayFunc; + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/HoverMode.cs b/src/FlaUInspect/Core/HoverMode.cs deleted file mode 100644 index b0c9541..0000000 --- a/src/FlaUInspect/Core/HoverMode.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Windows.Input; -using System.Windows.Threading; -using FlaUI.Core; -using FlaUI.Core.AutomationElements; -using FlaUInspect.Core.Logger; -using Mouse = FlaUI.Core.Input.Mouse; -using Point = System.Drawing.Point; - -namespace FlaUInspect.Core; - -public class HoverMode { - private readonly AutomationBase? _automation; - private readonly DispatcherTimer _dispatcherTimer; - private readonly ILogger? _logger; - private AutomationElement? _currentHoveredElement; - - public HoverMode(AutomationBase? automation, ILogger? logger) { - _automation = automation; - _logger = logger; - _dispatcherTimer = new DispatcherTimer(); - _dispatcherTimer.Tick += DispatcherTimerTick; - _dispatcherTimer.Interval = TimeSpan.FromMilliseconds(500); - } - - public event Action? ElementHovered; - - public void Start() { - _currentHoveredElement = null; - _dispatcherTimer.Start(); - } - - public void Stop() { - _currentHoveredElement = null; - _dispatcherTimer.Stop(); - } - - private void DispatcherTimerTick(object? sender, EventArgs e) { - if (Keyboard.Modifiers.HasFlag(ModifierKeys.Control)) { - Point screenPos = Mouse.Position; - - try { - AutomationElement? hoveredElement = _automation?.FromPoint(screenPos); - - // Skip items in the current process - // Like Inspect itself or the overlay window - if (hoveredElement?.Properties.ProcessId == Environment.ProcessId) { - return; - } - - if (!Equals(_currentHoveredElement, hoveredElement)) { - _currentHoveredElement = hoveredElement; - ElementHovered?.Invoke(hoveredElement); - } else { - ElementHighlighter.HighlightElement(hoveredElement, _logger); - } - } catch (UnauthorizedAccessException) { - _logger?.LogError("You are accessing a protected UI element in hover mode.\nTry to start FlaUInspect as administrator."); - } catch (Exception ex) { - _logger?.LogError($"Exception: {ex.Message}"); - } - } - } -} \ No newline at end of file diff --git a/src/FlaUInspect/Core/IDialogViewModel.cs b/src/FlaUInspect/Core/IDialogViewModel.cs new file mode 100644 index 0000000..973ca60 --- /dev/null +++ b/src/FlaUInspect/Core/IDialogViewModel.cs @@ -0,0 +1,12 @@ +namespace FlaUInspect.Core; + +public interface IDialogViewModel { + string Title { get; } + string CloseButtonText { get; } + string SaveButtonText { get; } + bool IsSaveVisible { get; } + bool IsCloseVisible { get; } + bool CanClose { get; } + void Save(); + void Close(); +} \ No newline at end of file diff --git a/src/FlaUInspect/Core/ObservableObject.cs b/src/FlaUInspect/Core/ObservableObject.cs index 055e256..f672d17 100644 --- a/src/FlaUInspect/Core/ObservableObject.cs +++ b/src/FlaUInspect/Core/ObservableObject.cs @@ -59,4 +59,14 @@ private static bool IsEqualInternal(T field, T newValue) { // Alternative: EqualityComparer.Default.Equals(field, newValue); return Equals(field, newValue); } + + public void RaisePropertyChanged([CallerMemberName] string propertyName = null) { + RaisePropertyChanged(new PropertyChangedEventArgs(propertyName)); + } + + protected virtual void RaisePropertyChanged(PropertyChangedEventArgs args) { + PropertyChangedEventHandler? copy = PropertyChanged; + if (copy != null) + copy(this, args); + } } \ No newline at end of file diff --git a/src/FlaUInspect/Core/RelayCommandAsync.cs b/src/FlaUInspect/Core/RelayCommandAsync.cs new file mode 100644 index 0000000..f86e688 --- /dev/null +++ b/src/FlaUInspect/Core/RelayCommandAsync.cs @@ -0,0 +1,80 @@ +using System.Windows.Input; + +namespace FlaUInspect.Core; + +public class AsyncRelayCommand : ObservableObject, ICommand { + private readonly Func _canExecute; + private readonly Func _execute; + private bool _isRunning = false; + + /// Initializes a new instance of the class. + /// The function to execute. + public AsyncRelayCommand(Func execute) + : this(execute, null) { + } + + /// Initializes a new instance of the class. + /// The function. + /// The predicate to check whether the function can be executed. + public AsyncRelayCommand(Func execute, Func canExecute) { + if (execute == null) + throw new ArgumentNullException("execute"); + + _execute = execute; + _canExecute = canExecute; + } + + /// Gets a value indicating whether the command is currently running. + public bool IsRunning { + get => _isRunning; + private set { + _isRunning = value; + RaiseCanExecuteChanged(); + } + } + + /// Gets a value indicating whether the command can execute in its current state. + public bool CanExecute => !IsRunning && (_canExecute == null || _canExecute()); + + /// Occurs when changes occur that affect whether or not the command should execute. + public event EventHandler CanExecuteChanged; + + void ICommand.Execute(object parameter) { + Execute(); + } + + bool ICommand.CanExecute(object parameter) { + return CanExecute; + } + + /// Defines the method to be called when the command is invoked. + protected async void Execute() { + Task? task = _execute(); + + if (task != null) { + IsRunning = true; + await task; + IsRunning = false; + } + } + + /// Gets a value indicating whether the command can execute in its current state. + /// Tries to execute the command by checking the property + /// and executes the command only when it can be executed. + /// True if command has been executed; false otherwise. + public bool TryExecute() { + if (!CanExecute) + return false; + Execute(); + return true; + } + + /// Triggers the CanExecuteChanged event and a property changed event on the CanExecute property. + public void RaiseCanExecuteChanged() { + RaisePropertyChanged(nameof(CanExecute)); + + EventHandler? copy = CanExecuteChanged; + if (copy != null) + copy(this, new EventArgs()); + } +} \ No newline at end of file diff --git a/src/FlaUInspect/FlaUInspect.csproj b/src/FlaUInspect/FlaUInspect.csproj index 25b3ea9..17e227f 100644 --- a/src/FlaUInspect/FlaUInspect.csproj +++ b/src/FlaUInspect/FlaUInspect.csproj @@ -16,7 +16,7 @@ FlaUInspect FlaUInspect FlaUInspect - 2.0.0 + 3.0.0 @@ -24,22 +24,13 @@ FlaUInspect - - TRACE;AUTOMATION_UIA2 - bin\UIA2\ - FlaUInspect.UIA2 - - - - TRACE;AUTOMATION_UIA3 - bin\UIA3\ - FlaUInspect.UIA3 - - - - - + + + + + + @@ -58,10 +49,80 @@ Wpf Designer - - MSBuild:Compile - Wpf - Designer + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer + + + MSBuild:Compile + Wpf + Designer @@ -73,7 +134,8 @@ - + + diff --git a/FlaUInspect.png b/src/FlaUInspect/FlaUInspect.png similarity index 100% rename from FlaUInspect.png rename to src/FlaUInspect/FlaUInspect.png diff --git a/src/FlaUInspect/Models/ElementPatternItem.cs b/src/FlaUInspect/Models/ElementPatternItem.cs index 1967250..e970bec 100644 --- a/src/FlaUInspect/Models/ElementPatternItem.cs +++ b/src/FlaUInspect/Models/ElementPatternItem.cs @@ -4,8 +4,7 @@ namespace FlaUInspect.Models; -public class ElementPatternItem(string patternName, string patternIdName, bool isVisible = true, bool isExpanded = false) - : ObservableObject { +public class ElementPatternItem(string patternName, string patternIdName, bool isVisible = true, bool isExpanded = false) : ObservableObject { private bool _isExpanded = isExpanded; private bool _isVisible = isVisible; @@ -22,5 +21,8 @@ public bool IsExpanded { } // ReSharper disable once CollectionNeverQueried.Global - public ObservableCollection Children { get; } = []; + public ObservableCollection? Children { + get => GetProperty>(); + set => SetProperty(value); + } } \ No newline at end of file diff --git a/src/FlaUInspect/Resources/DetailsIcons.xaml b/src/FlaUInspect/Resources/DetailsIcons.xaml index c67599a..66466b8 100644 --- a/src/FlaUInspect/Resources/DetailsIcons.xaml +++ b/src/FlaUInspect/Resources/DetailsIcons.xaml @@ -10,7 +10,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -46,7 +46,7 @@ - + @@ -64,7 +64,7 @@ - + @@ -82,7 +82,7 @@ - + @@ -100,7 +100,7 @@ - + @@ -118,7 +118,7 @@ - + @@ -136,7 +136,7 @@ - + @@ -154,7 +154,7 @@ - + @@ -172,7 +172,7 @@ - + @@ -190,7 +190,7 @@ - + @@ -208,7 +208,7 @@ - + @@ -226,7 +226,7 @@ - + @@ -244,7 +244,7 @@ - + @@ -262,7 +262,7 @@ - + @@ -280,7 +280,7 @@ - + @@ -298,7 +298,7 @@ - + @@ -316,7 +316,7 @@ - + @@ -334,7 +334,7 @@ - + @@ -352,7 +352,7 @@ - + @@ -370,7 +370,7 @@ - + @@ -388,7 +388,7 @@ - + @@ -406,7 +406,7 @@ - + @@ -424,7 +424,7 @@ - + @@ -442,7 +442,7 @@ - + @@ -460,7 +460,7 @@ - + @@ -478,7 +478,7 @@ - + @@ -496,7 +496,7 @@ - + @@ -514,7 +514,7 @@ - + @@ -532,7 +532,7 @@ - + @@ -550,7 +550,7 @@ - + @@ -568,7 +568,7 @@ - + @@ -586,7 +586,7 @@ - + diff --git a/src/FlaUInspect/Resources/Icons.xaml b/src/FlaUInspect/Resources/Icons.xaml index bc93fd1..70a9ab5 100644 --- a/src/FlaUInspect/Resources/Icons.xaml +++ b/src/FlaUInspect/Resources/Icons.xaml @@ -26,7 +26,7 @@ - + @@ -673,6 +673,24 @@ + + + + + + + + \ No newline at end of file diff --git a/src/FlaUInspect/Resources/RibbonIcons.xaml b/src/FlaUInspect/Resources/RibbonIcons.xaml index 6eca9c6..d5f8f26 100644 --- a/src/FlaUInspect/Resources/RibbonIcons.xaml +++ b/src/FlaUInspect/Resources/RibbonIcons.xaml @@ -2,199 +2,226 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - - - - - - - - - - - + + - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + + + + + + - - - - - + - + + + + + + + + + + + + + + - + - - - - - - - + + + + + + + + + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/FlaUInspect/Settings/FlaUiAppOptions.cs b/src/FlaUInspect/Settings/FlaUiAppOptions.cs new file mode 100644 index 0000000..b60ce36 --- /dev/null +++ b/src/FlaUInspect/Settings/FlaUiAppOptions.cs @@ -0,0 +1,16 @@ +using System.Drawing; +using System.Windows; +using FlaUInspect.Core; + +namespace FlaUInspect.Settings; + +public class FlaUiAppOptions { + public Func HoverOverlay { get; set; } = () => null; + public Func SelectionOverlay { get; set; } = () => null; + public Func PickOverlay { get; set; } = () => null; + + public Func DefaultOverlay { get; set; } = () => new ElementOverlay(new ElementOverlayConfiguration(2, + new Thickness(0), + Color.Red, + ElementOverlay.BoundRectangleFactory)); +} \ No newline at end of file diff --git a/src/FlaUInspect/Settings/FlaUiAppSettings.cs b/src/FlaUInspect/Settings/FlaUiAppSettings.cs new file mode 100644 index 0000000..b1c2465 --- /dev/null +++ b/src/FlaUInspect/Settings/FlaUiAppSettings.cs @@ -0,0 +1,45 @@ +using FlaUInspect.Core; + +namespace FlaUInspect.Settings; + +public class FlaUiAppSettings : ObservableObject, ICloneable { + private OverlaySettings? _hoverOverlay = new (); + private OverlaySettings? _pickOverlay = new (); + private OverlaySettings? _selectionOverlay = new (); + private string _theme = "Light"; + public string Theme { + get => _theme; + set => SetProperty(ref _theme, value); + } + + public OverlaySettings? HoverOverlay { + get => _hoverOverlay; + set => SetProperty(ref _hoverOverlay, value); + } + + public OverlaySettings? SelectionOverlay { + get => _selectionOverlay; + set => SetProperty(ref _selectionOverlay, value); + } + + public OverlaySettings? PickOverlay { + get => _pickOverlay; + set => SetProperty(ref _pickOverlay, value); + } + + public object Clone() { + return new FlaUiAppSettings { + Theme = Theme, + HoverOverlay = HoverOverlay?.Clone() as OverlaySettings, + SelectionOverlay = SelectionOverlay?.Clone() as OverlaySettings, + PickOverlay = PickOverlay?.Clone() as OverlaySettings + }; + } + + public void CopyTo(FlaUiAppSettings to) { + to.Theme = Theme; + to.PickOverlay?.CoppyTo(PickOverlay); + to.SelectionOverlay?.CoppyTo(SelectionOverlay); + to.HoverOverlay?.CoppyTo(HoverOverlay); + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Settings/ISettingViewModel.cs b/src/FlaUInspect/Settings/ISettingViewModel.cs new file mode 100644 index 0000000..75da37a --- /dev/null +++ b/src/FlaUInspect/Settings/ISettingViewModel.cs @@ -0,0 +1,7 @@ +using FlaUInspect.Core; + +namespace FlaUInspect.Settings; + +public interface ISettingViewModel { + Editable Settings { get; } +} \ No newline at end of file diff --git a/src/FlaUInspect/Settings/ISettingsService.cs b/src/FlaUInspect/Settings/ISettingsService.cs new file mode 100644 index 0000000..0d0187e --- /dev/null +++ b/src/FlaUInspect/Settings/ISettingsService.cs @@ -0,0 +1,6 @@ +namespace FlaUInspect.Settings; + +public interface ISettingsService where T : class, new() { + T Load(); + void Save(T settings); +} \ No newline at end of file diff --git a/src/FlaUInspect/Settings/JsonSettingsService.cs b/src/FlaUInspect/Settings/JsonSettingsService.cs new file mode 100644 index 0000000..94fc3b1 --- /dev/null +++ b/src/FlaUInspect/Settings/JsonSettingsService.cs @@ -0,0 +1,31 @@ +using System.IO; +using System.Text.Json; + +namespace FlaUInspect.Settings; + +public sealed class JsonSettingsService : ISettingsService where T : class, new() { + private readonly string _filePath; + private readonly JsonSerializerOptions _options; + + public JsonSettingsService(string filePath) { + _filePath = filePath; + + _options = new JsonSerializerOptions { + WriteIndented = true, + PropertyNameCaseInsensitive = true + }; + } + + public T Load() { + if (!File.Exists(_filePath)) + return new T(); + + string json = File.ReadAllText(_filePath); + return JsonSerializer.Deserialize(json, _options) ?? new T(); + } + + public void Save(T settings) { + string json = JsonSerializer.Serialize(settings, _options); + File.WriteAllText(_filePath, json); + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Settings/OverlaySettings.cs b/src/FlaUInspect/Settings/OverlaySettings.cs new file mode 100644 index 0000000..6bc48c1 --- /dev/null +++ b/src/FlaUInspect/Settings/OverlaySettings.cs @@ -0,0 +1,45 @@ +using FlaUInspect.Core; + +namespace FlaUInspect.Settings; + +public class OverlaySettings : ObservableObject, ICloneable { + private string _color = "#FFFF0000"; + private string _margin = "0"; + private string _overlayMode = "Bound"; + private int _size; + public int Size { + get => _size; + set => SetProperty(ref _size, value); + } + public string Margin { + get => _margin; + set => SetProperty(ref _margin, value); + } + public string OverlayColor { + get => _color; + set => SetProperty(ref _color, value); + } + public string OverlayMode { + get => _overlayMode; + set => SetProperty(ref _overlayMode, value); + } + + public object Clone() { + return new OverlaySettings { + Size = Size, + Margin = Margin, + OverlayColor = OverlayColor, + OverlayMode = OverlayMode + }; + } + + public void CoppyTo(OverlaySettings? to) { + if (to == null) { + return; + } + to.Size = Size; + to.Margin = Margin; + to.OverlayColor = OverlayColor; + to.OverlayMode = OverlayMode; + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Themes/DarkTheme.xaml b/src/FlaUInspect/Themes/DarkTheme.xaml new file mode 100644 index 0000000..aec9057 --- /dev/null +++ b/src/FlaUInspect/Themes/DarkTheme.xaml @@ -0,0 +1,45 @@ + + + #2B2D30 + #555659 + #7f8182 + #a9a9a9 + + #4E5157 + + #1E1E1E + #262626 + + #555659 + #007ACC + + #e9eaea + #a9a9a9 + + + + + + + + + + + + + + + + + + + + + 1,1,1,1 + 2 + + + + + \ No newline at end of file diff --git a/src/FlaUInspect/Themes/Generic.xaml b/src/FlaUInspect/Themes/Generic.xaml new file mode 100644 index 0000000..eb77916 --- /dev/null +++ b/src/FlaUInspect/Themes/Generic.xaml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/FlaUInspect/Themes/LightTheme.xaml b/src/FlaUInspect/Themes/LightTheme.xaml new file mode 100644 index 0000000..6b55e95 --- /dev/null +++ b/src/FlaUInspect/Themes/LightTheme.xaml @@ -0,0 +1,45 @@ + + + LightGray + #c0c0c0 + #a9a9a9 + #a9a9a9 + + #FF000000 + + #dddddd + #bebebe + + #F0F0F0 + #007ACC + + #000000 + #a9a9a9 + + + + + + + + + + + + + + + + + + + + + 1,1,1,1 + 2 + + + + + \ No newline at end of file diff --git a/src/FlaUInspect/ViewModels/AboutViewModel.cs b/src/FlaUInspect/ViewModels/AboutViewModel.cs new file mode 100644 index 0000000..45a433d --- /dev/null +++ b/src/FlaUInspect/ViewModels/AboutViewModel.cs @@ -0,0 +1,19 @@ +using FlaUInspect.Core; + +namespace FlaUInspect.ViewModels; + +public class AboutViewModel : ObservableObject, IDialogViewModel { + + public string Title { get; } = "About FlaUInspect"; + public string CloseButtonText { get; } = "Ok"; + public string SaveButtonText { get; } = ""; + public bool IsSaveVisible { get; } = false; + public bool IsCloseVisible { get; } = true; + public bool CanClose { get; } = true; + + public void Save() { + } + + public void Close() { + } +} \ No newline at end of file diff --git a/src/FlaUInspect/ViewModels/ElementViewModel.cs b/src/FlaUInspect/ViewModels/ElementViewModel.cs index 941daf1..cdefac6 100644 --- a/src/FlaUInspect/ViewModels/ElementViewModel.cs +++ b/src/FlaUInspect/ViewModels/ElementViewModel.cs @@ -7,19 +7,31 @@ namespace FlaUInspect.ViewModels; -public class ElementViewModel(AutomationElement? automationElement, ILogger? logger) : ObservableObject { +public class ElementViewModel : ObservableObject { + private readonly string _guidId; + private readonly object _lockObject = new (); - public AutomationElement? AutomationElement { get; } = automationElement; + private readonly ILogger? _logger; + + public ElementViewModel(AutomationElement? automationElement, ElementViewModel? parent, int level, ILogger? logger) { + Level = level; + _logger = logger; + AutomationElement = automationElement; + Parent = parent; + + _guidId = Guid.NewGuid().ToString() + (AutomationElement?.Properties.Name.ValueOrDefault ?? string.Empty).NormalizeString(); + Name = (AutomationElement?.Properties.Name.ValueOrDefault ?? string.Empty).NormalizeString(); + AutomationId = (AutomationElement?.Properties.AutomationId.ValueOrDefault ?? string.Empty).NormalizeString(); + ControlType = AutomationElement != null && AutomationElement.Properties.ControlType.TryGetValue(out ControlType value) ? value : ControlType.Unknown; + + } + + public AutomationElement? AutomationElement { get; } + public ElementViewModel? Parent { get; } public bool IsExpanded { get => GetProperty(); - set { - SetProperty(value); - - if (value && (Children.Count == 0 || Children[0] == null)) { - LoadChildren(0); - } - } + set => SetProperty(value); } public bool IsSelected { @@ -27,50 +39,36 @@ public bool IsSelected { set => SetProperty(value); } - public string Name => (AutomationElement?.Properties.Name.ValueOrDefault ?? string.Empty).NormalizeString(); - - public string AutomationId => (AutomationElement?.Properties.AutomationId.ValueOrDefault ?? string.Empty).NormalizeString(); + public int Level { get; } - public ControlType ControlType => AutomationElement != null && AutomationElement.Properties.ControlType.TryGetValue(out ControlType value) ? value : ControlType.Custom; - - public ExtendedObservableCollection Children { get; set; } = []; + public string Name { get; } + public string AutomationId { get; } + public ControlType ControlType { get; } public string XPath => AutomationElement == null ? string.Empty : Debug.GetXPathToElement(AutomationElement); - public event Action? SelectionChanged; - public void LoadChildren(int level) { - lock (_lockObject) { - if (Children is { Count: > 0 } && Children[0] == null) { - Children.Clear(); - } + public override string ToString() { + return $"{Name} [{ControlType}] : {AutomationId}"; + } - foreach (ElementViewModel? child in Children) { - child!.SelectionChanged -= SelectionChanged; - } + public List LoadChildren() { + { - List childrenViewModels = []; try { if (AutomationElement != null) { - foreach (AutomationElement child in AutomationElement.FindAllChildren()) { - ElementViewModel childViewModel = new (child, logger); - childViewModel.Children.Add(null); - - childViewModel.SelectionChanged += SelectionChanged; - childrenViewModels.Add(childViewModel); - - if (level > 0) { - childViewModel.LoadChildren(level - 1); + using (CacheRequest.ForceNoCache()) { + AutomationElement[] elements = AutomationElement.FindAllChildren(); - } + return elements.Select(element => new ElementViewModel(element, this, Level + 1, _logger)).ToList(); } } } catch (Exception ex) { - logger?.LogError($"Exception: {ex.Message}"); + _logger?.LogError($"Exception: {ex.Message}"); } - Children.Reset(childrenViewModels); + return []; } } } \ No newline at end of file diff --git a/src/FlaUInspect/ViewModels/MainViewModel.cs b/src/FlaUInspect/ViewModels/MainViewModel.cs deleted file mode 100644 index 2cbdae8..0000000 --- a/src/FlaUInspect/ViewModels/MainViewModel.cs +++ /dev/null @@ -1,340 +0,0 @@ -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Drawing; -using System.Drawing.Imaging; -using System.Reflection; -using System.Windows.Data; -using System.Windows.Input; -using FlaUI.Core; -using FlaUI.Core.AutomationElements; -using FlaUI.Core.Identifiers; -using FlaUI.UIA2; -using FlaUI.UIA3; -using FlaUInspect.Core; -using FlaUInspect.Core.Logger; -using FlaUInspect.Models; -using FlaUInspect.Views; -using Microsoft.Win32; -using Application = System.Windows.Application; - -namespace FlaUInspect.ViewModels; - -public class MainViewModel : ObservableObject { - - private readonly object _itemsLock = new (); - private readonly InternalLogger? _logger; - private AutomationBase? _automation; - private RelayCommand? _captureSelectedItemCommand; - private RelayCommand? _closeInfoCommand; - private ObservableCollection? _elementPatterns = []; - private FocusTrackingMode? _focusTrackingMode; - private HoverMode? _hoverMode; - private RelayCommand? _infoCommand; - private RelayCommand? _openErrorListCommand; - private PatternItemsFactory? _patternItemsFactory; - private RelayCommand? _refreshCommand; - private RelayCommand? _refreshItemCommand; - private AutomationElement? _rootElement; - private RelayCommand? _startNewInstanceCommand; - private ITreeWalker? _treeWalker; - - public MainViewModel(AutomationType automationType, string applicationVersion, InternalLogger logger) { - _logger = logger; - ApplicationVersion = applicationVersion; - _logger.LogEvent += (_, _) => { - Application.Current.Dispatcher.Invoke(() => ErrorCount = _logger.Messages.Count); - }; - - SelectedAutomationType = automationType; - Elements = []; - BindingOperations.EnableCollectionSynchronization(Elements, _itemsLock); - - } - - public ICommand OpenErrorListCommand => - _openErrorListCommand ??= new RelayCommand(_ => { - if (_logger is { Messages.IsEmpty: false }) { - ErrorListWindow errorListWindow = new (_logger); - errorListWindow.ShowDialog(); - } - }, - _ => !_logger?.Messages.IsEmpty ?? false); - - public int ErrorCount { - get => GetProperty(); - private set => SetProperty(value); - } - - public bool EnableHoverMode { - get => GetProperty(); - set { - if (SetProperty(value)) { - if (value) { - _hoverMode?.Start(); - } else { - _hoverMode?.Stop(); - } - } - } - } - - public bool EnableHighLightSelectionMode { - get => GetProperty(); - set => SetProperty(value); - } - - public bool EnableFocusTrackingMode { - get => GetProperty(); - set { - if (SetProperty(value)) { - if (value) { - _focusTrackingMode?.Start(); - } else { - _focusTrackingMode?.Stop(); - } - } - } - } - - public bool EnableXPath { - get => GetProperty(); - set => SetProperty(value); - } - - public AutomationType SelectedAutomationType { - get => GetProperty(); - private init => SetProperty(value); - } - - public ObservableCollection Elements { get; private set; } - - public ICommand StartNewInstanceCommand => - _startNewInstanceCommand ??= new RelayCommand(_ => { - ProcessStartInfo info = new (Assembly.GetExecutingAssembly().Location); - Process.Start(info); - }); - - public ICommand CaptureSelectedItemCommand => - _captureSelectedItemCommand ??= new RelayCommand(_ => { - if (SelectedItem?.AutomationElement == null) { - return; - } - Bitmap capturedImage = SelectedItem.AutomationElement.Capture(); - SaveFileDialog saveDialog = new () { - Filter = "Png file (*.png)|*.png" - }; - - if (saveDialog.ShowDialog() == true) { - capturedImage.Save(saveDialog.FileName, ImageFormat.Png); - } - capturedImage.Dispose(); - }); - - public ICommand RefreshCommand => - _refreshCommand ??= new RelayCommand(_ => { - EnableHoverMode = false; - EnableFocusTrackingMode = false; - EnableHighLightSelectionMode = false; - Elements.Clear(); - Initialize(); - }); - - public ElementViewModel? SelectedItem { - get => GetProperty(); - set { - if (SetProperty(value)) { - if (value != null) { - ReadPatternsForSelectedItem(value.AutomationElement); - } - } - } - } - - public ICommand RefreshItemCommand => - _refreshItemCommand ??= new RelayCommand(o => { - if (o is ElementViewModel item) { - item.Children.Clear(); - item.IsExpanded = true; - } - }); - - public IEnumerable ElementPatterns { - get => _elementPatterns ?? Enumerable.Empty(); - private set => SetProperty(ref _elementPatterns, value as ObservableCollection); - } - - public ICommand InfoCommand => _infoCommand ??= new RelayCommand(_ => { - IsInfoVisible = !IsInfoVisible; - }); - - public bool IsInfoVisible { - get => GetProperty(); - set => SetProperty(value); - } - public string? ApplicationVersion { - get => GetProperty(); - set => SetProperty(value); - } - - public ICommand CloseInfoCommand => _closeInfoCommand ??= new RelayCommand(_ => { - IsInfoVisible = false; - }); - - private void ReadPatternsForSelectedItem(AutomationElement? selectedItemAutomationElement) { - if (SelectedItem?.AutomationElement == null || selectedItemAutomationElement == null) { - return; - } - - if (_patternItemsFactory == null) { - return; - } - - if (EnableHighLightSelectionMode) { - ElementHighlighter.HighlightElement(SelectedItem.AutomationElement, _logger); - } - - try { - HashSet supportedPatterns = [.. selectedItemAutomationElement.GetSupportedPatterns()]; - IDictionary patternItemsForElement = _patternItemsFactory.CreatePatternItemsForElement(selectedItemAutomationElement, supportedPatterns); - - foreach (ElementPatternItem elementPattern in ElementPatterns) { - elementPattern.IsVisible = elementPattern.PatternIdName == PatternItemsFactory.Identification - || elementPattern.PatternIdName == PatternItemsFactory.Details - || elementPattern.PatternIdName == PatternItemsFactory.PatternSupport - || supportedPatterns.Any(x => x.Name.Equals(elementPattern.PatternIdName)); - elementPattern.Children.Clear(); - - if (patternItemsForElement.TryGetValue(elementPattern.PatternIdName, out PatternItem[]? children)) { - foreach (PatternItem patternItem in children) { - elementPattern.Children.Add(patternItem); - } - } - - if (!elementPattern.Children.Any()) { - elementPattern.IsVisible = false; - } - } - } catch (Exception e) { - _logger?.LogError(e.ToString()); - } - } - - public void Initialize() { - _automation = (SelectedAutomationType == AutomationType.UIA2 ? (AutomationBase?)new UIA2Automation() : new UIA3Automation()) ?? new UIA3Automation(); - _patternItemsFactory = new PatternItemsFactory(_automation); - _rootElement = _automation.GetDesktop(); - ElementViewModel desktopViewModel = new (_rootElement, _logger); - - desktopViewModel.SelectionChanged += obj => { - SelectedItem = obj; - }; - desktopViewModel.LoadChildren(0); - - lock (_itemsLock) { - Elements.Add(desktopViewModel); - desktopViewModel.IsExpanded = true; - } - - // Initialize TreeWalker - _treeWalker = _automation.TreeWalkerFactory.GetControlViewWalker(); - - // Initialize hover - _hoverMode = new HoverMode(_automation, _logger); - _hoverMode.ElementHovered += ElementToSelectChanged; - - // Initialize focus tracking - _focusTrackingMode = new FocusTrackingMode(_automation); - _focusTrackingMode.ElementFocused += ElementToSelectChanged; - - ElementPatterns = GetDefaultPatternList(); - SelectedItem = Elements[0]; - - OnPropertyChanged(nameof(Elements)); - OnPropertyChanged(nameof(ElementPatterns)); - } - - private ObservableCollection GetDefaultPatternList() { - return new ObservableCollection(new[] { - new ElementPatternItem("Identification", PatternItemsFactory.Identification, true, true), - new ElementPatternItem("Details", PatternItemsFactory.Details, true, true), - new ElementPatternItem("Pattern Support", PatternItemsFactory.PatternSupport, true, true) - } - .Concat( - (_automation?.PatternLibrary.AllForCurrentFramework ?? []) - .Select(x => { - ElementPatternItem patternItem = new (x.Name, x.Name) { - IsVisible = true - }; - return patternItem; - }))); - } - - private void ElementToSelectChanged(AutomationElement? obj) { - // Build a stack from the root to the hovered item - Stack pathToRoot = new (); - - while (obj != null) { - // Break on circular relationship (should not happen?) - if (pathToRoot.Contains(obj) || obj.Equals(_rootElement)) { - break; - } - - pathToRoot.Push(obj); - - try { - obj = _treeWalker?.GetParent(obj); - } catch (Exception ex) { - _logger?.LogError($"Exception: {ex.Message}"); - } - } - - // Expand the root element if needed - if (!Elements[0].IsExpanded) { - Elements[0].IsExpanded = true; - } - - ElementViewModel elementVm = Elements[0]; - - while (pathToRoot.Count > 0) { - AutomationElement elementOnPath = pathToRoot.Pop(); - ElementViewModel? nextElementVm = FindElement(elementVm, elementOnPath); - - if (nextElementVm == null) { - // Could not find next element, try reloading the parent - elementVm.LoadChildren(0); - // Now search again - nextElementVm = FindElement(elementVm, elementOnPath); - - if (nextElementVm == null) { - // The next element is still not found, exit the loop - _logger?.LogError("Could not find the next element!"); - break; - } - } - elementVm = nextElementVm; - - if (!elementVm.IsExpanded) { - elementVm.IsExpanded = true; - } - } - // Select the last element - SelectedItem = elementVm; - SelectedItem.IsSelected = true; - } - - private ElementViewModel? FindElement(ElementViewModel parent, AutomationElement element) { - return parent.Children.FirstOrDefault(child => { - if (child?.AutomationElement == null) { - return false; - } - - try { - return child.AutomationElement.Equals(element); - } catch (Exception e) { - _logger?.LogError(e.ToString()); - } - - return false; - }); - } -} \ No newline at end of file diff --git a/src/FlaUInspect/ViewModels/ProcessViewModel.cs b/src/FlaUInspect/ViewModels/ProcessViewModel.cs new file mode 100644 index 0000000..2c8940b --- /dev/null +++ b/src/FlaUInspect/ViewModels/ProcessViewModel.cs @@ -0,0 +1,402 @@ +using System.Collections.ObjectModel; +using System.Drawing; +using System.Drawing.Imaging; +using System.Windows; +using System.Windows.Input; +using FlaUI.Core; +using FlaUI.Core.AutomationElements; +using FlaUI.Core.Identifiers; +using FlaUInspect.Core; +using FlaUInspect.Core.Exporters; +using FlaUInspect.Core.Logger; +using FlaUInspect.Models; +using Microsoft.Win32; + +namespace FlaUInspect.ViewModels; + +public class ProcessViewModel : ObservableObject { + + private readonly AutomationBase _automation; + private readonly InternalLogger _logger; + private readonly int _processId; + private readonly ITreeWalker _treeWalker; + private readonly IntPtr _windowHandle; + private ObservableCollection? _elementPatterns; + private FocusTrackingMode? _focusTrackingMode; + private PatternItemsFactory? _patternItemsFactory; + private AutomationElement? _rootElement; + private ElementOverlay _trackHighlighterOverlay; + + public ProcessViewModel(AutomationBase automation, int processId, IntPtr mainWindowHandle, InternalLogger logger) { + _logger = logger; + _automation = automation; + _processId = processId; + _windowHandle = mainWindowHandle; + + _trackHighlighterOverlay = CreateTrackHighlighterOverlay(); + + WindowTitle = $"Process: [{processId}] '{(processId != 0 + ? _automation.FromHandle(mainWindowHandle)?.Properties.Name ?? "N/A" + : "Desktop")}'"; + + HoverManager.AddListener(_windowHandle, + x => { + if (EnableHoverMode) { + ElementToSelectChanged(x); + } + }); + HoverManager.Disable(_windowHandle); + + _treeWalker = _automation.TreeWalkerFactory.GetControlViewWalker(); + + Elements = []; + + RefreshCommand = new AsyncRelayCommand(async () => await Task.Run(Initialize)); + CaptureSelectedItemCommand = new RelayCommand(_ => { + if (SelectedItem?.AutomationElement == null) { + return; + } + Bitmap capturedImage = SelectedItem.AutomationElement.Capture(); + SaveFileDialog saveDialog = new () { + Filter = "Png file (*.png)|*.png" + }; + + if (saveDialog.ShowDialog() == true) { + capturedImage.Save(saveDialog.FileName, ImageFormat.Png); + } + capturedImage.Dispose(); + }); + + CurrentElementSaveStateCommand = new RelayCommand(_ => { + if (SelectedItem?.AutomationElement == null) { + return; + } + + try { + ITreeExporter exporter = new XmlTreeExporter(EnableXPath); + string exportedTree = exporter.Export(SelectedItem); + + Clipboard.SetText(exportedTree.ToString()); + CopiedNotificationCurrentElementSaveStateRequested?.Invoke(); + } catch (Exception e) { + _logger?.LogError(e.ToString()); + } + }); + + ClosingCommand = new RelayCommand(_ => { + HoverManager.RemoveListener(_windowHandle); + _trackHighlighterOverlay?.Dispose(); + _focusTrackingMode?.Stop(); + _focusTrackingMode = null; + }); + + CopyDetailsToClipboardCommand = new RelayCommand(_ => { + if (SelectedItem?.AutomationElement == null) { + return; + } + + try { + IElementDetailsExporter detailsExporter = new XmlElementDetailsExporter(); + string details = detailsExporter.Export(ElementPatterns); + + Clipboard.SetText(details); + CopiedNotificationRequested?.Invoke(); + } catch (Exception e) { + _logger?.LogError(e.ToString()); + } + }); + } + + public string? WindowTitle { get; } + + + public bool EnableXPath { + get => GetProperty(); + set => SetProperty(value); + } + + public ObservableCollection Elements { get; private set; } + public ObservableCollection? FlatNodes { + get => GetProperty>(); + private set => SetProperty(value); + } + + public IEnumerable ElementPatterns { + get => _elementPatterns ?? Enumerable.Empty(); + private set => SetProperty(ref _elementPatterns, value as ObservableCollection); + } + + public ElementViewModel? SelectedItem { + get => GetProperty(); + set { + if (SetProperty(value)) { + if (value != null) { + if (EnableHighLightSelectionMode) { + TrackSelectedItem(value); + } + Task.Run(() => ReadPatternsForSelectedItem(value.AutomationElement)); + } + } + } + } + + public bool EnableHoverMode { + get => GetProperty(); + set { + SetProperty(value); + SetMode(); + } + } + + public bool EnableHighLightSelectionMode { + get => GetProperty(); + set { + SetProperty(value); + SetMode(); + } + } + + public ICommand ClosingCommand { get; } + public ICommand RefreshCommand { get; } + public ICommand CaptureSelectedItemCommand { get; } + public ICommand CurrentElementSaveStateCommand { get; } + public ICommand CopyDetailsToClipboardCommand { get; } + + public bool EnableFocusTrackingMode { + get => GetProperty(); + set { + SetProperty(value); + SetMode(); + } + } + + private static ElementOverlay CreateTrackHighlighterOverlay() { + return App.FlaUiAppOptions.SelectionOverlay() ?? App.FlaUiAppOptions.DefaultOverlay()!; + } + + private void TrackSelectedItem(ElementViewModel item) { + if (item.AutomationElement != null) { + _trackHighlighterOverlay?.Dispose(); + _trackHighlighterOverlay = CreateTrackHighlighterOverlay(); + + try { + _trackHighlighterOverlay.Show(item.AutomationElement.Properties.BoundingRectangle.Value); + } catch (Exception e) { + _trackHighlighterOverlay?.Dispose(); + } + } + } + + private void SetMode() { + HoverManager.Disable(_windowHandle); + _trackHighlighterOverlay?.Dispose(); + _focusTrackingMode?.Stop(); + + if (new[] { EnableHoverMode, EnableHighLightSelectionMode, EnableFocusTrackingMode }.Count(x => x) == 1) { + if (EnableFocusTrackingMode) { + _focusTrackingMode?.Start(); + } else if (EnableHighLightSelectionMode) { + if (SelectedItem != null) { + TrackSelectedItem(SelectedItem); + } + } else if (EnableHoverMode) { + HoverManager.Enable(_windowHandle); + } + } + } + + public event Action? CopiedNotificationCurrentElementSaveStateRequested; + public event Action CopiedNotificationRequested; + + public void Initialize() { + _patternItemsFactory = new PatternItemsFactory(_automation); + + _rootElement = _windowHandle == IntPtr.Zero + ? _automation.GetDesktop() + : _automation.FromHandle(_windowHandle); + + ElementViewModel desktopViewModel = new (_rootElement, null, 0, _logger); + + List topChildren = desktopViewModel.LoadChildren(); + + Elements = new ObservableCollection(topChildren); + + // Initialize hover + EnableHoverMode = false; + + // Initialize focus tracking + _focusTrackingMode = new FocusTrackingMode(_automation, + x => { + if (EnableFocusTrackingMode) { + ElementToSelectChanged(x); + } + }); + + ElementPatterns = GetDefaultPatternList(); + SelectedItem = Elements.Count == 0 ? null : Elements[0]; + + OnPropertyChanged(nameof(Elements)); + OnPropertyChanged(nameof(ElementPatterns)); + } + + public void ElementToSelectChanged(AutomationElement? obj, bool forceExpand = false) { + Stack pathToRoot = new (); + + while (obj != null && obj.Properties.ProcessId == _processId) { + // Break on circular relationship (should not happen?) + if (pathToRoot.Contains(obj) || obj.Equals(_rootElement)) { + break; + } + + pathToRoot.Push(obj); + + if (forceExpand) { + break; + } + + try { + obj = _treeWalker.GetParent(obj); + } catch (Exception ex) { + _logger?.LogError($"Exception: {ex.Message}"); + } + } + + IEnumerable viewModels = Elements; + ElementViewModel? nextElementVm = null; + + while (pathToRoot.Count > 0) { + AutomationElement elementOnPath = pathToRoot.Pop(); + nextElementVm = FindElement(viewModels, elementOnPath); + + if (nextElementVm != null && (forceExpand || !nextElementVm.IsExpanded)) { + if (pathToRoot.Count != 0) { + nextElementVm.IsExpanded = true; + } + ExpandElement(nextElementVm); + + if (forceExpand) { + break; + } + } + } + + SelectedItem = nextElementVm; + } + + private ElementViewModel? FindElement(IEnumerable viewModels, AutomationElement element) { + return viewModels.FirstOrDefault(el => { + if (el?.AutomationElement == null) { + return false; + } + + try { + return el.AutomationElement.Equals(element); + } catch (Exception e) { + _logger?.LogError(e.ToString()); + } + + return false; + }); + } + + private ObservableCollection GetDefaultPatternList() { + return new ObservableCollection(new[] { + new ElementPatternItem("Identification", PatternItemsFactory.Identification, true, true), + new ElementPatternItem("Details", PatternItemsFactory.Details, true, true), + new ElementPatternItem("Pattern Support", PatternItemsFactory.PatternSupport, true, true) + } + .Concat( + (_automation?.PatternLibrary.AllForCurrentFramework ?? []) + .Select(x => { + ElementPatternItem patternItem = new (x.Name, x.Name) { + IsVisible = true + }; + return patternItem; + }))); + } + + + private void ReadPatternsForSelectedItem(AutomationElement? selectedItemAutomationElement) { + if (SelectedItem?.AutomationElement == null || selectedItemAutomationElement == null) { + return; + } + + if (_patternItemsFactory == null) { + return; + } + + try { + HashSet supportedPatterns = [.. selectedItemAutomationElement.GetSupportedPatterns()]; + IDictionary patternItemsForElement = _patternItemsFactory.CreatePatternItemsForElement(selectedItemAutomationElement, supportedPatterns); + + foreach (ElementPatternItem elementPattern in ElementPatterns) { + elementPattern.IsVisible = elementPattern.PatternIdName == PatternItemsFactory.Identification + || elementPattern.PatternIdName == PatternItemsFactory.Details + || elementPattern.PatternIdName == PatternItemsFactory.PatternSupport + || supportedPatterns.Any(x => x.Name.Equals(elementPattern.PatternIdName)); + + + elementPattern.Children = patternItemsForElement.TryGetValue(elementPattern.PatternIdName, out PatternItem[]? children) + ? new ObservableCollection(children) + : []; + + if (!elementPattern.Children.Any()) { + elementPattern.IsVisible = false; + } + } + } catch (Exception e) { + _logger?.LogError(e.ToString()); + } + } + + public void ExpandElement(ElementViewModel sender) { + List children = sender.LoadChildren(); + children.Reverse(); + + int senderIndex = Elements.IndexOf(sender); + + if (senderIndex < 0) { + return; + } + + foreach (ElementViewModel child in children) { + Elements.Insert(senderIndex + 1, child); + } + } + + public void CollapseElement(ElementViewModel sender) { + int senderIndex = Elements.IndexOf(sender); + + if (senderIndex < 0) { + return; + } + + var removeCount = 0; + + for (int i = senderIndex + 1; i < Elements.Count; i++) { + if (IsDescendantOf(Elements[i], sender)) { + removeCount++; + } else { + break; + } + } + + for (var i = 0; i < removeCount; i++) { + Elements.RemoveAt(senderIndex + 1); + } + } + + private bool IsDescendantOf(ElementViewModel? node, ElementViewModel? parent) { + if (node == null || parent == null) { + return false; + } + ElementViewModel? p = node.Parent; + + while (p != null) { + if (p == parent) + return true; + p = p.Parent; + } + return false; + } +} \ No newline at end of file diff --git a/src/FlaUInspect/ViewModels/SettingsViewModel.cs b/src/FlaUInspect/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..b19e3f4 --- /dev/null +++ b/src/FlaUInspect/ViewModels/SettingsViewModel.cs @@ -0,0 +1,39 @@ +using FlaUInspect.Core; +using FlaUInspect.Settings; +using Microsoft.Extensions.DependencyInjection; + +namespace FlaUInspect.ViewModels; + +public class SettingsViewModel : ObservableObject, IDialogViewModel, ISettingViewModel { + private readonly ISettingsService _settingsService; + + public SettingsViewModel() { + _settingsService = App.Services.GetRequiredService>(); + FlaUiAppSettings flaUiAppSettings = _settingsService.Load(); + Settings = new Editable(flaUiAppSettings, + s => (s.Clone() as FlaUiAppSettings)!, + (from, to) => from.CopyTo(to), + (a, b) => a.Equals(b)); + } + + public IEnumerable Themes { get; } = new List { "Light", "Dark" }; + public IEnumerable OverlayModes { get; } = new List { "Fill", "Border" }; + + public void Save() { + _settingsService.Save(Settings.Current); + } + + public string Title { get; } = "Settings"; + public string CloseButtonText { get; } = "Close"; + public string SaveButtonText { get; } = "Save"; + public bool IsSaveVisible { get; } = true; + public bool IsCloseVisible { get; } = true; + + public bool CanClose { get; } = true; + + public void Close() { + + } + + public Editable Settings { get; } +} \ No newline at end of file diff --git a/src/FlaUInspect/ViewModels/StartupViewModel.cs b/src/FlaUInspect/ViewModels/StartupViewModel.cs new file mode 100644 index 0000000..85bb862 --- /dev/null +++ b/src/FlaUInspect/ViewModels/StartupViewModel.cs @@ -0,0 +1,320 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Data; +using System.Windows.Input; +using FlaUI.Core; +using FlaUI.Core.AutomationElements; +using FlaUI.Core.Definitions; +using FlaUI.UIA3; +using FlaUInspect.Core; +using FlaUInspect.Settings; +using Microsoft.Extensions.DependencyInjection; +using Application = System.Windows.Application; + +namespace FlaUInspect.ViewModels; + +public class StartupViewModel : ObservableObject { + private const int WhMouseLl = 14; + private const uint GaRoot = 2; + + private static IntPtr _mouseHook = IntPtr.Zero; + private static LowLevelMouseProc? _mouseProc; + + private readonly AutomationBase _defaultAutomation = new UIA3Automation(); + + private ICollectionView _filteredProcesses; + + private ObservableCollection _processes = []; + private ElementOverlay? _topWindowOverlay; + private AutomationElement? _topWindowUnderCursor; + + public StartupViewModel() { + IsWindowedOnly = true; + RefreshCommand = new AsyncRelayCommand(async () => { + await Init(); + }); + + PickCommand = new AsyncRelayCommand(async () => { + using CancellationTokenSource cts = new (TimeSpan.FromSeconds(30)); + IntPtr hwnd = await PickWindowAsync(cts.Token); + SelectedProcess = Processes.FirstOrDefault(x => x.MainWindowHandle == hwnd); + }); + + SettingCommand = new RelayCommand(_ => { + ISettingsService? settingsService = App.Services.GetService>(); + FlaUiAppSettings flaUiAppSettings = settingsService.Load(); + Editable settings = new (flaUiAppSettings, + s => s.Clone() as FlaUiAppSettings, + (from, to) => from.CopyTo(to), + (a, b) => a.Equals(b)); + + DialogContent = new SettingsViewModel(); + }); + + CloseSettingCommand = new RelayCommand(_ => { + if (DialogContent is IDialogViewModel { CanClose: true } closableViewModel) { + closableViewModel.Close(); + DialogContent = null; + } + }, + _ => DialogContent is IDialogViewModel { CanClose: true }); + + SaveSettingCommand = new RelayCommand(_ => { + if (DialogContent is IDialogViewModel { CanClose: true } closableViewModel) { + ISettingViewModel? settingsViewModel = DialogContent as ISettingViewModel; + closableViewModel.Save(); + DialogContent = null; + + if (settingsViewModel != null) { + App.ApplyAppOption(settingsViewModel.Settings.Current); + } + } + }, + _ => DialogContent is IDialogViewModel { CanClose: true }); + AboutCommand = new RelayCommand(_=> { + DialogContent = new AboutViewModel(); + }); + + _filteredProcesses = CollectionViewSource.GetDefaultView(_processes); + _filteredProcesses.Filter = FilterProcesses; + + DialogContent = null; + } + + + public object? DialogContent { + get => GetProperty(); + set => SetProperty(value); + } + + public ICollectionView FilteredProcesses => _filteredProcesses; + + public ICommand SettingCommand { get; private set; } + public ICommand RefreshCommand { get; private set; } + public ICommand PickCommand { get; } + + public ICommand CloseSettingCommand { get; } + public ICommand SaveSettingCommand { get; } + + public ICommand AboutCommand { get; } + + + public bool IsBusy { + get => GetProperty(); + set => SetProperty(value); + } + + public ProcessWindowInfo? SelectedProcess { + get => GetProperty(); + set => SetProperty(value); + } + + public ObservableCollection Processes { + get => _processes; + private set { + SetProperty(ref _processes, value); + _filteredProcesses = CollectionViewSource.GetDefaultView(_processes); + _filteredProcesses.Filter = FilterProcesses; + OnPropertyChanged(nameof(FilteredProcesses)); + } + } + + public string? FilterProcess { + get => GetProperty(); + set { + if (SetProperty(value)) { + _filteredProcesses?.Refresh(); + } + } + } + + public bool IsWindowedOnly { + get => GetProperty(); + set { + if (SetProperty(value)) { + Task.Run(async () => await Init()); + } + } + } + + private bool FilterProcesses(object obj) { + if (obj is not ProcessWindowInfo p) { + return false; + } + + if (string.IsNullOrWhiteSpace(FilterProcess)) { + return true; + } + + return p.WindowTitle.Contains(FilterProcess, StringComparison.OrdinalIgnoreCase) + || p.ProcessId.ToString().Contains(FilterProcess, StringComparison.OrdinalIgnoreCase); + } + + private async Task PickWindowAsync(CancellationToken ctsToken) { + Cursor? previousCursor = Mouse.OverrideCursor; + Mouse.OverrideCursor = Cursors.Cross; + + try { + return await WaitForMouseClickWindowAsync(ctsToken); + } catch (OperationCanceledException) { + // canceled - ignore + } finally { + Application.Current.Dispatcher.Invoke(() => Mouse.OverrideCursor = previousCursor); + } + return IntPtr.Zero; + } + + private Task WaitForMouseClickWindowAsync(CancellationToken ct) { + TaskCompletionSource tcs = new (TaskCreationOptions.RunContinuationsAsynchronously); + + _mouseProc = (nCode, wParam, lParam) => { + const int WM_LBUTTONDOWN = 0x0201; + const int WM_LBUTTONUP = 0x0202; + + if (nCode >= 0 && (wParam == (IntPtr)WM_LBUTTONUP || wParam == (IntPtr)WM_LBUTTONDOWN || wParam == (IntPtr)0x0200)) { + if (GetCursorPos(out POINT pt)) { + IntPtr hwnd = WindowFromPoint(pt); + IntPtr root = GetAncestor(hwnd, GaRoot); + + // Highlight the window under the mouse, but skip if it is the current process + GetWindowThreadProcessId(root, out uint windowProcessId); + + if (windowProcessId != (uint)Process.GetCurrentProcess().Id) { + AutomationElement? topWindowUnderCursor = GetTopWindowUnderCursor(); + + if (_topWindowUnderCursor == null || !_topWindowUnderCursor.Equals(topWindowUnderCursor)) { + _topWindowOverlay?.Dispose(); + + try { + Rectangle boundingRectangleValue = topWindowUnderCursor.Properties.BoundingRectangle.Value; + _topWindowOverlay = App.FlaUiAppOptions.PickOverlay(); + _topWindowOverlay?.Show(boundingRectangleValue); + _topWindowUnderCursor = topWindowUnderCursor; + + SelectedProcess = Processes.FirstOrDefault(x => x.MainWindowHandle == topWindowUnderCursor.Properties.NativeWindowHandle); + } catch { + // Ignore exceptions when getting bounding rectangle + } + } + + } else { + _topWindowOverlay?.Dispose(); + _topWindowUnderCursor = null; + } + + if (wParam == (IntPtr)WM_LBUTTONUP) { + _topWindowOverlay?.Dispose(); + _topWindowUnderCursor = null; + + if (windowProcessId != (uint)Process.GetCurrentProcess().Id) { + tcs.TrySetResult(root); + } else { + tcs.TrySetResult(IntPtr.Zero); + } + } + } + } + return CallNextHookEx(_mouseHook, nCode, wParam, lParam); + }; + + try { + IntPtr hMod = GetModuleHandle(Process.GetCurrentProcess().MainModule?.ModuleName ?? string.Empty); + _mouseHook = SetWindowsHookEx(WhMouseLl, _mouseProc!, hMod, 0); + } catch { + // If hook fails, set result zero + tcs.TrySetResult(IntPtr.Zero); + } + + if (ct.CanBeCanceled) { + ct.Register(() => { + tcs.TrySetCanceled(); + + if (_mouseHook != IntPtr.Zero) { + UnhookWindowsHookEx(_mouseHook); + _mouseHook = IntPtr.Zero; + } + }); + } + + return tcs.Task.ContinueWith(t => { + if (_mouseHook != IntPtr.Zero) { + UnhookWindowsHookEx(_mouseHook); + _mouseHook = IntPtr.Zero; + } + return t.IsCompletedSuccessfully ? t.Result : IntPtr.Zero; + }, + TaskScheduler.Default); + } + + public AutomationElement? GetTopWindowUnderCursor() { + if (!GetCursorPos(out POINT pt)) + return null; + + IntPtr hwnd = WindowFromPoint(pt); + if (hwnd == IntPtr.Zero) + return null; + + IntPtr rootHwnd = GetAncestor(hwnd, GaRoot); + if (rootHwnd == IntPtr.Zero) + return null; + + return _defaultAutomation?.FromHandle(rootHwnd); + } + + public async Task Init() { + IsBusy = true; + await Task.Delay(100); // Simulate some loading time; + int currentProcessId = Environment.ProcessId; + IEnumerable collection = GetChildren(_defaultAutomation.GetDesktop()) + .Where(x => !string.IsNullOrEmpty(x.Name)) + .Where(x => x.Properties.ProcessId != currentProcessId) + .Select(x => new ProcessWindowInfo(x.Properties.ProcessId.Value, + x.Name, + x.Properties.NativeWindowHandle.Value)) + .ToList(); + Processes = new ObservableCollection(collection); + IsBusy = false; + return; + + AutomationElement[] GetChildren(AutomationElement el) { + return IsWindowedOnly ? el.FindAllChildren(x => x.ByControlType(ControlType.Window)) : el.FindAllChildren(); + } + } + + [DllImport("user32.dll")] + private static extern bool GetCursorPos(out POINT lpPoint); + + [DllImport("user32.dll")] + private static extern IntPtr WindowFromPoint(POINT point); + + [DllImport("user32.dll")] + private static extern IntPtr GetAncestor(IntPtr hwnd, uint gaFlags); + + [DllImport("user32.dll")] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool UnhookWindowsHookEx(IntPtr hhk); + + [DllImport("user32.dll")] + private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr GetModuleHandle(string lpModuleName); + + private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam); + + [StructLayout(LayoutKind.Sequential)] + private struct POINT { + public int X; + public int Y; + } +} + +public record ProcessWindowInfo(int ProcessId, string WindowTitle, IntPtr MainWindowHandle); \ No newline at end of file diff --git a/src/FlaUInspect/Views/AboutView.xaml b/src/FlaUInspect/Views/AboutView.xaml new file mode 100644 index 0000000..311a79b --- /dev/null +++ b/src/FlaUInspect/Views/AboutView.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/FlaUInspect/Views/AboutView.xaml.cs b/src/FlaUInspect/Views/AboutView.xaml.cs new file mode 100644 index 0000000..06d8a21 --- /dev/null +++ b/src/FlaUInspect/Views/AboutView.xaml.cs @@ -0,0 +1,90 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Shapes; + +namespace FlaUInspect.Views; + +public partial class AboutView : UserControl { + private readonly Random _rnd = new (); + private CancellationTokenSource? _colorLoopCts; + + public AboutView() { + InitializeComponent(); + Loaded += (_, __) => { + Regenerate(); + StartTextColorLoop(); + }; + SizeChanged += (_, __) => Regenerate(); + } + + private void StartTextColorLoop() { + _colorLoopCts?.Cancel(); + _colorLoopCts = new CancellationTokenSource(); + + _ = RunColorLoop(_colorLoopCts.Token); + } + + + private void Regenerate() { + if (HudCanvas.ActualWidth <= 0 || HudCanvas.ActualHeight <= 0) + return; + + HudCanvas.Children.Clear(); + + var count = 50; + double w = HudCanvas.ActualWidth; + double h = HudCanvas.ActualHeight; + + for (var i = 0; i < count; i++) { + Line line = new() { + X1 = _rnd.NextDouble() * w, + Y1 = _rnd.NextDouble() * h, + X2 = _rnd.NextDouble() * w, + Y2 = _rnd.NextDouble() * h, + Style = (Style)Resources["HudLine"], + Stroke = new SolidColorBrush(RandomHudColor()) + }; + + HudCanvas.Children.Add(line); + + Storyboard pulse = ((Storyboard)Resources["PulseLine"]).Clone(); + Storyboard.SetTarget(pulse, line); + + foreach (Timeline? t in pulse.Children) + t.BeginTime = TimeSpan.FromMilliseconds(_rnd.Next(0, 1200)); + + pulse.Begin(); + } + } + + private async Task RunColorLoop(CancellationToken token) { + while (!token.IsCancellationRequested) { + Color next = RandomHudColor(); + + ColorAnimation anim = new() { + To = next, + Duration = TimeSpan.FromSeconds(2.5), + EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut } + }; + + FlaUiControlBrush.BeginAnimation(SolidColorBrush.ColorProperty, anim); + + // пауза між переходами + await Task.Delay(TimeSpan.FromSeconds(3.0), token); + } + } + + private Color RandomHudColor() { + Color[] palette = { + Color.FromArgb(180, 80, 200, 255), + Color.FromArgb(180, 120, 160, 255), + Color.FromArgb(180, 160, 120, 255), + Color.FromArgb(180, 80, 255, 200), + Color.FromArgb(180, 200, 120, 255) + }; + + return palette[_rnd.Next(palette.Length)]; + } +} \ No newline at end of file diff --git a/src/FlaUInspect/Views/ChooseVersionWindow.xaml b/src/FlaUInspect/Views/ChooseVersionWindow.xaml deleted file mode 100644 index 4c9619f..0000000 --- a/src/FlaUInspect/Views/ChooseVersionWindow.xaml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/FlaUInspect/Views/ChooseVersionWindow.xaml.cs b/src/FlaUInspect/Views/ChooseVersionWindow.xaml.cs deleted file mode 100644 index 94e257b..0000000 --- a/src/FlaUInspect/Views/ChooseVersionWindow.xaml.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Windows; -using FlaUI.Core; - -namespace FlaUInspect.Views; - -/// -/// Interaction logic for ChooseVersionWindow.xaml -/// -public partial class ChooseVersionWindow { - public ChooseVersionWindow() { - InitializeComponent(); - } - - public AutomationType SelectedAutomationType { get; private set; } - - private void Uia2ButtonClick(object sender, RoutedEventArgs e) { - SelectedAutomationType = AutomationType.UIA2; - DialogResult = true; - } - - private void Uia3ButtonClick(object sender, RoutedEventArgs e) { - SelectedAutomationType = AutomationType.UIA3; - DialogResult = true; - } -} \ No newline at end of file diff --git a/src/FlaUInspect/Views/MainWindow.xaml b/src/FlaUInspect/Views/MainWindow.xaml deleted file mode 100644 index 824f95d..0000000 --- a/src/FlaUInspect/Views/MainWindow.xaml +++ /dev/null @@ -1,469 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -