1
0
Fork 0
mirror of https://github.com/hawkeye-stan/msfs-popout-panel-manager.git synced 2024-11-25 23:30:10 +00:00

Version 2.0

This commit is contained in:
hawkeye 2021-09-29 22:17:20 -04:00
parent ac49940706
commit b842428d27
87 changed files with 3921 additions and 1387 deletions

View file

@ -1,279 +0,0 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using Tesseract;
namespace MSFSPopoutPanelManager
{
public class AnalysisEngine
{
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumChildWindows(IntPtr window, EnumWindowProc callback, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern Int32 GetClassName(IntPtr hWnd, StringBuilder StrPtrClassName, Int32 nMaxCount);
[DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
[DllImport("user32.dll", ExactSpelling = false, CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpWindowText, int nMaxCount);
[DllImport("user32.dll")]
public static extern bool SetWindowText(System.IntPtr hwnd, System.String lpString);
[DllImport("user32.dll")]
private static extern int GetWindowRect(IntPtr hwnd, out Rect lpRect);
[DllImport("user32.dll")]
static extern bool MoveWindow(IntPtr hWnd, int x, int y, int width, int height, bool repaint);
[DllImport("user32.dll")]
public static extern IntPtr SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int x, int y, int cx, int cy, uint wFlags);
[DllImport("user32.dll")]
static extern bool SetForegroundWindow(IntPtr hWnd);
private delegate bool EnumWindowProc(IntPtr hwnd, IntPtr lParam);
const int SWP_SHOWWINDOW = 0x0040;
public AnalysisEngine() { }
public event EventHandler<EventArgs<string>> OnStatusUpdated;
public event EventHandler<EventArgs<Dictionary<string, string>>> OnOcrDebugged;
public void Analyze(ref MainWindow simWindow, OcrEvalData ocrEvaluationData)
{
MainWindow processZeroMainWindow = null;
// Get process with PID of zero (PID zero launches all the popout windows for MSFS)
foreach (var process in Process.GetProcesses())
{
if(process.Id == 0)
processZeroMainWindow = new MainWindow()
{
ProcessId = process.Id,
ProcessName = process.ProcessName,
Title = "Process Zero",
Handle = process.MainWindowHandle
};
}
// Get all child windows
GetChildWindows(simWindow);
GetChildWindows(processZeroMainWindow);
// The popout windows such as PFD and MFD attached itself to main window for Process ID zero instead of the MSFS process.
// Moving these windows back into MSFS main window
if (processZeroMainWindow != null)
{
foreach (var child in processZeroMainWindow.ChildWindowsData)
{
int parentProcessId;
GetWindowThreadProcessId(child.Handle, out parentProcessId);
if (simWindow != null && parentProcessId == simWindow.ProcessId)
simWindow.ChildWindowsData.Add(child);
}
}
int classNameLength = 256;
foreach (var childWindow in simWindow.ChildWindowsData)
{
StringBuilder className = new StringBuilder(classNameLength);
GetClassName(childWindow.Handle, className, classNameLength);
childWindow.ClassName = className.ToString();
}
// Remove all child windows where class name is not 'AceApp' and try to determine popout type
simWindow.ChildWindowsData.RemoveAll(x => x.ClassName != "AceApp" || (x.Title != null && x.Title.Contains("Microsoft Flight Simulator", StringComparison.CurrentCultureIgnoreCase)));
simWindow.ChildWindowsData = simWindow.ChildWindowsData.GroupBy(x => x.Handle).Select(g => g.First()).ToList();
simWindow.ChildWindowsData.ForEach(x => {
if (String.IsNullOrEmpty(x.Title) || x.Title.Contains("Custom - ") || x.Title.Contains("Failed Analysis"))
x.PopoutType = PopoutType.Custom;
else
x.PopoutType = PopoutType.BuiltIn;
});
if(simWindow.ChildWindowsData.Count > 0)
{
var ocrImageScale = ocrEvaluationData.OCRImageScale;
Dictionary<string, string> debugInfo = new Dictionary<string, string>();
foreach (var childWindow in simWindow.ChildWindowsData)
{
if (childWindow.PopoutType == PopoutType.Custom)
{
// Figure out what windows is what?
// Theses are the windows with no system menu bar title (ie. PFD, MFD, FMS, etc)
// We need to take a screenshot and do OCR to try to figure them out
Rect rect = new Rect();
GetWindowRect(childWindow.Handle, out rect);
// Set window to be custom scale (eaiser to OCR) and set to foreground before taking screenshot image
var originalWidth = rect.Right - rect.Left;
var originalHeight = rect.Bottom - rect.Top;
var newWidth = Convert.ToInt32(originalWidth * ocrImageScale);
var newHeight = Convert.ToInt32(originalHeight * ocrImageScale);
rect.Right = rect.Left + newWidth;
rect.Bottom = rect.Top + newHeight;
MoveWindow(childWindow.Handle, rect.Left, rect.Top, newWidth, newHeight, true);
SetForegroundWindow(childWindow.Handle);
Thread.Sleep(500);
var image = TakeScreenShot(rect, childWindow.Handle.ToInt32());
MoveWindow(childWindow.Handle, rect.Left, rect.Top, originalWidth, originalHeight, true);
// OCR the image into text
var imageText = OcrImage(image);
var popoutName = EvaluateImageText(imageText, ocrEvaluationData);
childWindow.Title = popoutName == null ? $"Failed Analysis - {childWindow.Handle}" : $"Custom - {popoutName}";
childWindow.PopoutType = popoutName == null ? PopoutType.Undetermined : PopoutType.Custom;
SetWindowText(childWindow.Handle, childWindow.Title);
if (!debugInfo.TryAdd(childWindow.Title, imageText))
debugInfo.Add($"{childWindow.Title} - {childWindow.Handle}", imageText);
}
}
// Output debug info
if(debugInfo.Count > 0)
OnOcrDebugged?.Invoke(this, new EventArgs<Dictionary<string, string>>(debugInfo));
// Remove all windows that cannot be identified
simWindow.ChildWindowsData.RemoveAll(x => x.Title == null);
OnStatusUpdated?.Invoke(this, new EventArgs<string>("Anaylsis completed."));
}
else
{
OnStatusUpdated?.Invoke(this, new EventArgs<string>("No pop out panels to analyze."));
}
}
private void GetChildWindows(MainWindow mainWindow)
{
var childHandles = GetAllChildHandles(mainWindow.Handle);
childHandles.ForEach(childHandle =>
{
mainWindow.ChildWindowsData.Add(new ChildWindow
{
Handle = childHandle,
Title = GetWindowTitle(childHandle)
});
});
}
private string GetWindowTitle(IntPtr hWnd)
{
StringBuilder title = new StringBuilder(1024);
GetWindowText(hWnd, title, title.Capacity);
return String.IsNullOrEmpty(title.ToString()) ? null : title.ToString();
}
private List<IntPtr> GetAllChildHandles(IntPtr parent)
{
var childHandles = new List<IntPtr>();
GCHandle gcChildhandlesList = GCHandle.Alloc(childHandles);
IntPtr pointerChildHandlesList = GCHandle.ToIntPtr(gcChildhandlesList);
try
{
EnumWindowProc childProc = new EnumWindowProc(EnumWindow);
EnumChildWindows(parent, childProc, pointerChildHandlesList);
}
finally
{
gcChildhandlesList.Free();
}
return childHandles;
}
private bool EnumWindow(IntPtr hWnd, IntPtr lParam)
{
var gcChildhandlesList = GCHandle.FromIntPtr(lParam);
if (gcChildhandlesList.Target == null)
return false;
var childHandles = gcChildhandlesList.Target as List<IntPtr>;
childHandles.Add(hWnd);
return true;
}
private byte[] TakeScreenShot(Rect rect, int imageId)
{
var bounds = new System.Drawing.Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);
var bitmap = new Bitmap(bounds.Width, bounds.Height);
using (var g = Graphics.FromImage(bitmap))
{
g.CopyFromScreen(new System.Drawing.Point(bounds.Left, bounds.Top), System.Drawing.Point.Empty, bounds.Size);
}
var converter = new ImageConverter();
var imageBytes = (byte[])converter.ConvertTo(bitmap, typeof(byte[]));
// Convert image to grayscale and invert the color to give tesseract a better image to analyze
using(var img = SixLabors.ImageSharp.Image.Load(imageBytes))
{
img.Mutate(x => x.Invert().GaussianSharpen().Grayscale());
double imageRatio = 250 / 72.0; // change from default of 72 DPI to 250 DPI
img.Metadata.HorizontalResolution *= imageRatio;
img.Metadata.VerticalResolution *= imageRatio;
var memoryStream = new MemoryStream();
img.Save(memoryStream, new SixLabors.ImageSharp.Formats.Bmp.BmpEncoder());
#if DEBUG
Directory.CreateDirectory("imageDebug");
img.Save(@$"./imageDebug/test-{imageId}.jpg");
#endif
return memoryStream.ToArray();
}
}
private string OcrImage(byte[] imageByteArray)
{
var ocrengine = new TesseractEngine(@".\tessdata", "eng", EngineMode.Default);
var iamge = Pix.LoadFromMemory(imageByteArray);
var result = ocrengine.Process(iamge);
return result.GetText();
}
private string EvaluateImageText(string imageText, OcrEvalData ocrEvaluationData)
{
if(ocrEvaluationData != null)
{
var popoutNames = ocrEvaluationData.EvalData.Select(x => x.PopoutName).Distinct();
foreach(var popoutName in popoutNames)
{
if (ocrEvaluationData.EvalData.Find(x => x.PopoutName == popoutName).Data.Any(s => imageText.Contains(s.ToLower(), StringComparison.CurrentCultureIgnoreCase)))
return popoutName;
}
}
return null;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

View file

@ -1,59 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
namespace MSFSPopoutPanelManager
{
public class FileManager
{
private string _startupPath;
public FileManager(string startupPath)
{
_startupPath = startupPath;
}
public UserData ReadUserData()
{
try
{
using (StreamReader reader = new StreamReader(_startupPath + @"\config\userdata.json"))
{
string json = reader.ReadToEnd();
return JsonConvert.DeserializeObject<UserData>(json);
}
}
catch
{
return null;
}
}
public void WriteUserData(UserData userData)
{
using (StreamWriter file = File.CreateText(_startupPath + @"\config\userdata.json"))
{
JsonSerializer serializer = new JsonSerializer();
serializer.Serialize(file, userData);
}
}
public List<OcrEvalData> ReadProfileData()
{
using (StreamReader reader = new StreamReader(_startupPath + @"\config\ocrdata.json"))
{
string json = reader.ReadToEnd();
try
{
return JsonConvert.DeserializeObject<List<OcrEvalData>>(json);
}
catch
{
throw new Exception("The file ocrdata.json is invalid.");
}
}
}
}
}

View file

@ -1,14 +0,0 @@
using System;
namespace MSFSPopoutPanelManager
{
public class EventArgs<T> : EventArgs
{
public T Value { get; private set; }
public EventArgs(T val)
{
Value = val;
}
}
}

View file

@ -5,15 +5,22 @@
<TargetFramework>net5.0-windows</TargetFramework> <TargetFramework>net5.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<Platforms>x64</Platforms> <Platforms>x64</Platforms>
<Version>1.2</Version> <Version>2.0</Version>
<AssemblyName>MSFSPopoutPanelManager</AssemblyName> <AssemblyName>MSFSPopoutPanelManager</AssemblyName>
<RootNamespace>MSFSPopoutPanelManager</RootNamespace> <RootNamespace>MSFSPopoutPanelManager</RootNamespace>
<ApplicationIcon>WindowManager.ico</ApplicationIcon> <ApplicationIcon>WindowManager.ico</ApplicationIcon>
<Authors>Stanley Kwok</Authors> <Authors>Stanley Kwok</Authors>
<Product>MSFS 2020 Popout Window Manager</Product> <Product>MSFS 2020 Popout Window Manager</Product>
<PackageId>MSFS 2020 Popout Panel Manager</PackageId> <PackageId>MSFS 2020 Popout Panel Manager</PackageId>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Remove="ImageLibrary\**" />
<EmbeddedResource Remove="ImageLibrary\**" />
<None Remove="ImageLibrary\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="UIAutomationClient"> <Reference Include="UIAutomationClient">
<HintPath>..\..\..\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\UIAutomationClient.dll</HintPath> <HintPath>..\..\..\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\UIAutomationClient.dll</HintPath>
@ -29,49 +36,39 @@
<ItemGroup> <ItemGroup>
<None Remove="C:\Users\hawkeye\.nuget\packages\tesseract\4.1.1\build\\..\x86\tesseract41.dll" /> <None Remove="C:\Users\hawkeye\.nuget\packages\tesseract\4.1.1\build\\..\x86\tesseract41.dll" />
<None Remove="Config\AnalysisData\analysisconfig.json" />
<None Remove="Config\planeprofile.json" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AForge" Version="2.2.5" />
<PackageReference Include="AForge.Imaging" Version="2.2.5" />
<PackageReference Include="DarkUI" Version="2.0.2" />
<PackageReference Include="MouseKeyHook" Version="5.6.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.3" />
<PackageReference Include="Tesseract" Version="4.1.1" />
<PackageReference Include="Tesseract.Data.English" Version="4.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Remove="C:\Users\hawkeye\.nuget\packages\tesseract.data.english\4.0.0\build\tessdata\eng.traineddata" /> <Content Remove="C:\Users\hawkeye\.nuget\packages\tesseract.data.english\4.0.0\build\tessdata\eng.traineddata" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<NativeLibs Remove="MainWindow.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<NativeLibs Remove="ChildWindow.cs" /> <NativeLibs Remove="ChildWindow.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="config\ocrdata.json"> <Content Include="Config\planeprofile.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Config\AnalysisData\analysisconfig.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<NativeLibs Remove="OcrEvalData.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Rect.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<NativeLibs Remove="WindowManager.cs" /> <NativeLibs Remove="WindowManager.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<NativeLibs Remove="MainForm.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<NativeLibs Remove="FileManager.cs" /> <NativeLibs Remove="FileManager.cs" />
</ItemGroup> </ItemGroup>
@ -80,10 +77,6 @@
<NativeLibs Remove="UserData.cs" /> <NativeLibs Remove="UserData.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<NativeLibs Remove="GenericEventArgs.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<NativeLibs Remove="README.md" /> <NativeLibs Remove="README.md" />
</ItemGroup> </ItemGroup>
@ -92,15 +85,155 @@
<NativeLibs Remove="AnalysisEngine.cs" /> <NativeLibs Remove="AnalysisEngine.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="images\" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<NativeLibs Remove="LICENSE" /> <NativeLibs Remove="LICENSE" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="Config\AnalysisData\cj4\cj4_mfd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\cj4\cj4_multipurpose_control.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\cj4\cj4_pfd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\cj4\cj4_standby_altitude_module.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g1000\g1000_mfd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g1000\g1000_mfd2.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g1000\g1000_pfd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g3000-kingair\g3000kingair_mfd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g3000-kingair\g3000kingair_mfd2.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g3000-kingair\g3000kingair_pfd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g3000-kingair\g3000kingair_pfd2.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g3000-kingair\g3000kingair_standby_altitude_module.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g3000\g3000_mfd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g3000\g3000_mfd2.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g3000\g3000_multipurpose_control.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g3000\g3000_pfd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g3000\g3000_standby_altitude_module_1.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g3000\g3000_standby_altitude_module_2.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\matching\A320NX\a320nx_enginedisplay.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\a32nx\a32nx_engine_display.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\matching\A320NX\a320nx_messagepanel.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\a32nx\a32nx_message_panel.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\matching\A320NX\a320nx_multipurposecontrol.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\a32nx\a32nx_multipurpose_control.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\matching\A320NX\a320nx_navdisplay.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\a32nx\a32nx_nav_display.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\a32nx\a32nx_pfd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\matching\A320NX\a320nx_standbyaltitudeindication.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="config\matching\A320NX\a320nx_standby_altitude_indication.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\a32nx\a32nx_standby_altitude_indicator.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\matching\A320NX\a320nx_systemdisplay.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\a32nx\a32nx_system_display.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g1000nxi\g1000nxi_mfd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g1000nxi\g1000nxi_mfd2.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\AnalysisData\g1000nxi\g1000nxi_pfd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\PreprocessingData\separation_button_qhd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\PreprocessingData\separation_button_uhd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\preprocess\blockmatch_wqhd1.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\preprocess\G1000MFD.PNG">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\preprocess\G1000PFD.PNG">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\preprocess\G1000\g1000_mfd_wqhd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\preprocess\g1000_mfd_wqhd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\preprocess\g1000_pfd_wqhd..PNG">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\preprocess\G1000\g1000_pfd_wqhd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\preprocess\g1000_pfd_wqhd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Config\PreprocessingData\separation_button_hd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\preprocessingdata\separation_button_wqhd.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="images\transparent.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="LICENSE"> <None Update="LICENSE">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
@ -119,4 +252,207 @@
<NativeLibs Remove="Vesion.md" /> <NativeLibs Remove="Vesion.md" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<NativeLibs Remove="PInvoke.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Logger.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Config\PreprocessingData\separation_button_hd.png" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Config\PreprocessingData\separation_button_wqhd.png" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="ImageOperation.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Config\PreprocessingData\g1000_pfd_wqhd.png" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Config\PreprocessingData\g1000_mfd_wqhd.png" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="PopoutCoorOverlay.Designer.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="PopoutCoorOverlay.resx" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="StartupForm.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="StartupForm.Designer.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="StartupForm.resx" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="UserControlPopOutStep.Designer.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="UserControlPopOutStep.resx" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="UserControlSeparateStep.Designer.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="UserControlSeparateStep.resx" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="UserControlStepAnalyze.Designer.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="UserControlStepAnalyze.resx" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="UserControlStepApplySettings.Designer.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="UserControlStepApplySettings.resx" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="PanelManager.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="UserControlCommon.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="UserControlCommon.resx" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="UserControlApplySettingsStep.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="UserControlPopOutStep.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="PopoutCoorOverlayForm.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Properties\Resources.resx" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="IdentifyPanelLocationModule.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="WindowManagementModule.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="PanelAnalysisModule.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="WindowProcess.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Config\AnalysisData\analysisconfig.json" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="images\seperation_analysis.png" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="images\screenshot1.png" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Config\planeprofile.json" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Config\AnalysisData\g1000nxi\g1000nxi_mfd.png" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Config\AnalysisData\g1000nxi\g1000nxi_pfd.png" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Config\AnalysisData\g1000nxi\g1000nxi_mfd2.png" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Modules\Enums.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="DataFileObjects.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Modules\PlaneProfile.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Modules\Structs.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Modules\ImageAnalysis.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="UI\UserControlIdentifyPopOut.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="UI\UserControlApplySettings.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Modules\PanelLocationSelectionModule.cs" />
</ItemGroup>
<ItemGroup>
<NativeLibs Remove="Modules\WindowManager.cs" />
</ItemGroup>
</Project> </Project>

261
MainForm.Designer.cs generated
View file

@ -1,261 +0,0 @@

namespace MSFSPopoutPanelManager
{
partial class MainForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
this.btnAnalyze = new System.Windows.Forms.Button();
this.btnSaveSettings = new System.Windows.Forms.Button();
this.comboBoxProfile = new System.Windows.Forms.ComboBox();
this.label1 = new System.Windows.Forms.Label();
this.txtStatus = new System.Windows.Forms.TextBox();
this.label3 = new System.Windows.Forms.Label();
this.lblMsfsRunning = new System.Windows.Forms.Label();
this.label4 = new System.Windows.Forms.Label();
this.tabControlOcrDebug = new System.Windows.Forms.TabControl();
this.chkHidePanelTitleBar = new System.Windows.Forms.CheckBox();
this.notifyIcon1 = new System.Windows.Forms.NotifyIcon(this.components);
this.checkBoxMinimizeToTray = new System.Windows.Forms.CheckBox();
this.lblVersion = new System.Windows.Forms.Label();
this.chkAlwaysOnTop = new System.Windows.Forms.CheckBox();
this.btnApplySettings = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// btnAnalyze
//
this.btnAnalyze.Enabled = false;
this.btnAnalyze.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.btnAnalyze.Location = new System.Drawing.Point(85, 60);
this.btnAnalyze.Name = "btnAnalyze";
this.btnAnalyze.Size = new System.Drawing.Size(103, 31);
this.btnAnalyze.TabIndex = 2;
this.btnAnalyze.Text = "Analyze";
this.btnAnalyze.UseVisualStyleBackColor = true;
this.btnAnalyze.Click += new System.EventHandler(this.btnAnalyze_Click);
//
// btnSaveSettings
//
this.btnSaveSettings.Enabled = false;
this.btnSaveSettings.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.btnSaveSettings.Location = new System.Drawing.Point(364, 60);
this.btnSaveSettings.Name = "btnSaveSettings";
this.btnSaveSettings.Size = new System.Drawing.Size(115, 31);
this.btnSaveSettings.TabIndex = 3;
this.btnSaveSettings.Text = "Save Settings";
this.btnSaveSettings.UseVisualStyleBackColor = true;
this.btnSaveSettings.Click += new System.EventHandler(this.btnSaveSettings_Click);
//
// comboBoxProfile
//
this.comboBoxProfile.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.comboBoxProfile.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.comboBoxProfile.FormattingEnabled = true;
this.comboBoxProfile.Location = new System.Drawing.Point(85, 21);
this.comboBoxProfile.Name = "comboBoxProfile";
this.comboBoxProfile.Size = new System.Drawing.Size(212, 28);
this.comboBoxProfile.TabIndex = 1;
this.comboBoxProfile.SelectedIndexChanged += new System.EventHandler(this.comboBoxProfile_SelectedIndexChanged);
//
// label1
//
this.label1.AutoSize = true;
this.label1.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.label1.Location = new System.Drawing.Point(23, 26);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(52, 20);
this.label1.TabIndex = 4;
this.label1.Text = "Profile";
//
// txtStatus
//
this.txtStatus.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.txtStatus.Location = new System.Drawing.Point(85, 108);
this.txtStatus.Multiline = true;
this.txtStatus.Name = "txtStatus";
this.txtStatus.ReadOnly = true;
this.txtStatus.Size = new System.Drawing.Size(606, 47);
this.txtStatus.TabIndex = 5;
//
// label3
//
this.label3.AutoSize = true;
this.label3.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.label3.Location = new System.Drawing.Point(23, 111);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(49, 20);
this.label3.TabIndex = 8;
this.label3.Text = "Status";
//
// lblMsfsRunning
//
this.lblMsfsRunning.AutoSize = true;
this.lblMsfsRunning.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.lblMsfsRunning.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.lblMsfsRunning.ForeColor = System.Drawing.Color.Red;
this.lblMsfsRunning.Location = new System.Drawing.Point(546, 429);
this.lblMsfsRunning.Name = "lblMsfsRunning";
this.lblMsfsRunning.Size = new System.Drawing.Size(145, 22);
this.lblMsfsRunning.TabIndex = 9;
this.lblMsfsRunning.Text = "MSFS is not Running";
this.lblMsfsRunning.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// label4
//
this.label4.AutoSize = true;
this.label4.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.label4.Location = new System.Drawing.Point(23, 174);
this.label4.Name = "label4";
this.label4.Size = new System.Drawing.Size(169, 20);
this.label4.TabIndex = 10;
this.label4.Text = "OCR Debug Information";
//
// tabControlOcrDebug
//
this.tabControlOcrDebug.Location = new System.Drawing.Point(26, 197);
this.tabControlOcrDebug.Name = "tabControlOcrDebug";
this.tabControlOcrDebug.SelectedIndex = 0;
this.tabControlOcrDebug.Size = new System.Drawing.Size(665, 220);
this.tabControlOcrDebug.TabIndex = 12;
//
// chkHidePanelTitleBar
//
this.chkHidePanelTitleBar.AutoSize = true;
this.chkHidePanelTitleBar.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.chkHidePanelTitleBar.Location = new System.Drawing.Point(317, 22);
this.chkHidePanelTitleBar.Name = "chkHidePanelTitleBar";
this.chkHidePanelTitleBar.Size = new System.Drawing.Size(158, 24);
this.chkHidePanelTitleBar.TabIndex = 15;
this.chkHidePanelTitleBar.Text = "Hide Panel Title Bar";
this.chkHidePanelTitleBar.UseVisualStyleBackColor = true;
//
// notifyIcon1
//
this.notifyIcon1.Icon = ((System.Drawing.Icon)(resources.GetObject("notifyIcon1.Icon")));
this.notifyIcon1.Text = "MSFS 2020 Pop OUt Panel Manager";
this.notifyIcon1.Visible = true;
this.notifyIcon1.DoubleClick += new System.EventHandler(this.notifyIcon1_DoubleClick);
//
// checkBoxMinimizeToTray
//
this.checkBoxMinimizeToTray.AutoSize = true;
this.checkBoxMinimizeToTray.Checked = true;
this.checkBoxMinimizeToTray.CheckState = System.Windows.Forms.CheckState.Checked;
this.checkBoxMinimizeToTray.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.checkBoxMinimizeToTray.Location = new System.Drawing.Point(26, 427);
this.checkBoxMinimizeToTray.Name = "checkBoxMinimizeToTray";
this.checkBoxMinimizeToTray.Size = new System.Drawing.Size(189, 24);
this.checkBoxMinimizeToTray.TabIndex = 16;
this.checkBoxMinimizeToTray.Text = "Minimize to System Tray";
this.checkBoxMinimizeToTray.UseVisualStyleBackColor = true;
//
// lblVersion
//
this.lblVersion.AutoSize = true;
this.lblVersion.Location = new System.Drawing.Point(305, 448);
this.lblVersion.Name = "lblVersion";
this.lblVersion.Size = new System.Drawing.Size(51, 15);
this.lblVersion.TabIndex = 17;
this.lblVersion.Text = "Version: ";
//
// chkAlwaysOnTop
//
this.chkAlwaysOnTop.AutoSize = true;
this.chkAlwaysOnTop.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.chkAlwaysOnTop.Location = new System.Drawing.Point(495, 22);
this.chkAlwaysOnTop.Name = "chkAlwaysOnTop";
this.chkAlwaysOnTop.Size = new System.Drawing.Size(124, 24);
this.chkAlwaysOnTop.TabIndex = 18;
this.chkAlwaysOnTop.Text = "Always on Top";
this.chkAlwaysOnTop.UseVisualStyleBackColor = true;
//
// btnApplySettings
//
this.btnApplySettings.Enabled = false;
this.btnApplySettings.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.btnApplySettings.Location = new System.Drawing.Point(216, 60);
this.btnApplySettings.Name = "btnApplySettings";
this.btnApplySettings.Size = new System.Drawing.Size(119, 31);
this.btnApplySettings.TabIndex = 19;
this.btnApplySettings.Text = "Apply Settings";
this.btnApplySettings.UseVisualStyleBackColor = true;
this.btnApplySettings.Click += new System.EventHandler(this.btnApplySettings_Click);
//
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(713, 472);
this.Controls.Add(this.btnApplySettings);
this.Controls.Add(this.chkAlwaysOnTop);
this.Controls.Add(this.lblVersion);
this.Controls.Add(this.checkBoxMinimizeToTray);
this.Controls.Add(this.chkHidePanelTitleBar);
this.Controls.Add(this.tabControlOcrDebug);
this.Controls.Add(this.label4);
this.Controls.Add(this.lblMsfsRunning);
this.Controls.Add(this.label3);
this.Controls.Add(this.txtStatus);
this.Controls.Add(this.label1);
this.Controls.Add(this.comboBoxProfile);
this.Controls.Add(this.btnSaveSettings);
this.Controls.Add(this.btnAnalyze);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.Name = "MainForm";
this.Text = "MSFS 2020 Pop Out Panel Manager";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing);
this.Load += new System.EventHandler(this.MainForm_Load);
this.Resize += new System.EventHandler(this.MainForm_Resize);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button btnAnalyze;
private System.Windows.Forms.Button btnSaveSettings;
private System.Windows.Forms.ComboBox comboBoxProfile;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox txtStatus;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.Label lblMsfsRunning;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.TabControl tabControlOcrDebug;
private System.Windows.Forms.CheckBox chkHidePanelTitleBar;
private System.Windows.Forms.NotifyIcon notifyIcon1;
private System.Windows.Forms.CheckBox checkBoxMinimizeToTray;
private System.Windows.Forms.Label lblVersion;
private System.Windows.Forms.CheckBox chkAlwaysOnTop;
private System.Windows.Forms.Button btnApplySettings;
}
}

View file

@ -1,221 +0,0 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
namespace MSFSPopoutPanelManager
{
public partial class MainForm : Form
{
private SynchronizationContext _syncRoot;
private FileManager _fileManager;
private WindowManager _windowManager;
private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
private const UInt32 SWP_NOSIZE = 0x0001;
private const UInt32 SWP_NOMOVE = 0x0002;
private const UInt32 TOPMOST_FLAGS = SWP_NOMOVE | SWP_NOSIZE;
[DllImport("user32.dll", EntryPoint = "SetWindowPos")]
public static extern IntPtr SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int Y, int cx, int cy, uint wFlags);
public MainForm()
{
InitializeComponent();
_syncRoot = SynchronizationContext.Current;
_fileManager = new FileManager(Application.StartupPath);
_windowManager = new WindowManager(_fileManager);
_windowManager.OnStatusUpdated += HandleOnStatusUpdated;
_windowManager.OnSimulatorStarted += HandleOnSimulatorStarted;
_windowManager.OnOcrDebugged += HandleOnOcrDebugged;
_windowManager.CheckSimulatorStarted();
SetProfileDropDown();
#if DEBUG
// Set application windows always on top for easy debugging
SetWindowPos(this.Handle, HWND_TOPMOST, this.Left, this.Top, this.Width, this.Height, TOPMOST_FLAGS);
#endif
var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
lblVersion.Text += version.ToString();
}
private void btnAnalyze_Click(object sender, EventArgs e)
{
txtStatus.Clear();
var profile = GetProfileDropDown();
var success = _windowManager.Analyze(profile);
btnApplySettings.Enabled = success;
btnSaveSettings.Enabled = success;
}
private void btnApplySettings_Click(object sender, EventArgs e)
{
txtStatus.Clear();
var profile = GetProfileDropDown();
_windowManager.ApplySettings(profile, chkHidePanelTitleBar.Checked, chkAlwaysOnTop.Checked);
}
private void btnSaveSettings_Click(object sender, EventArgs e)
{
txtStatus.Clear();
var profile = GetProfileDropDown();
_windowManager.SaveSettings(profile, chkHidePanelTitleBar.Checked, chkAlwaysOnTop.Checked);
}
private void SetProfileDropDown()
{
try
{
var profileData = _fileManager.ReadProfileData();
var profiles = profileData.Select(x => x.Profile).Distinct().OrderBy(x => x);
var defaultProfile = profileData.Find(x => x.DefaultProfile);
comboBoxProfile.DataSource = profiles.ToList();
comboBoxProfile.SelectedItem = defaultProfile.Profile;
}
catch (Exception ex)
{
SetStatusMessage(ex.Message);
}
}
private string GetProfileDropDown()
{
return comboBoxProfile.SelectedItem.ToString();
}
private void comboBoxProfile_SelectedIndexChanged(object sender, EventArgs e)
{
_windowManager.Reset();
var userData = _fileManager.ReadUserData();
if (userData != null)
{
var userProfile = userData.Profiles.Find(x => x.Name == Convert.ToString(comboBoxProfile.SelectedValue));
if (userProfile != null)
{
chkAlwaysOnTop.Checked = userProfile.AlwaysOnTop;
chkHidePanelTitleBar.Checked = userProfile.HidePanelTitleBar;
}
else
{
// default values
chkAlwaysOnTop.Checked = false;
chkHidePanelTitleBar.Checked = false;
}
}
btnApplySettings.Enabled = false;
btnSaveSettings.Enabled = false;
}
private void HandleOnStatusUpdated(object source, EventArgs<string> arg)
{
_syncRoot.Post(SetStatusMessage, arg.Value);
}
private void SetStatusMessage(object arg)
{
var msg = arg as string;
if (msg != null)
txtStatus.Text = msg;
}
private void HandleOnSimulatorStarted(object source, EventArgs arg)
{
_syncRoot.Post(SetMsfsRunningMessage, "MSFS is running");
}
private void SetMsfsRunningMessage(object arg)
{
var msg = arg as string;
if (msg != null)
{
lblMsfsRunning.Text = "MSFS is running";
lblMsfsRunning.ForeColor = Color.Green;
}
btnAnalyze.Enabled = true;
}
private void HandleOnOcrDebugged(object source, EventArgs<Dictionary<string, string>> arg)
{
_syncRoot.Post(SetOcrDebugInfo, arg.Value);
}
private void SetOcrDebugInfo(object arg)
{
tabControlOcrDebug.TabPages.Clear();
var debugInfo = arg as Dictionary<string, string>;
if (debugInfo != null && debugInfo.Count > 0)
{
foreach(var info in debugInfo)
{
var tabPage = new TabPage();
tabPage.Name = info.Key;
tabPage.Text = info.Key;
var txtBox = new TextBox();
txtBox.Width = tabControlOcrDebug.Width;
txtBox.Height = tabControlOcrDebug.Height;
txtBox.ReadOnly = true;
txtBox.Multiline = true;
txtBox.BorderStyle = BorderStyle.None;
txtBox.Text = info.Value;
tabPage.Controls.Add(txtBox);
tabControlOcrDebug.TabPages.Add(tabPage);
}
}
}
private void MainForm_Load(object sender, EventArgs e)
{
notifyIcon1.BalloonTipText = "Application Minimized";
notifyIcon1.BalloonTipTitle = "MSFS 2020 Pop Out Panel Manager";
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
// Put all panels popout back to original state
_windowManager.RestorePanelTitleBar();
}
private void MainForm_Resize(object sender, EventArgs e)
{
if (this.WindowState == FormWindowState.Minimized)
{
if (checkBoxMinimizeToTray.Checked)
{
ShowInTaskbar = false;
notifyIcon1.Visible = true;
notifyIcon1.ShowBalloonTip(1000);
}
}
}
private void notifyIcon1_DoubleClick(object sender, EventArgs e)
{
ShowInTaskbar = true;
notifyIcon1.Visible = false;
WindowState = FormWindowState.Normal;
}
}
}

View file

@ -6,24 +6,15 @@ namespace MSFSPopoutPanelManager
{ {
public ChildWindow() public ChildWindow()
{ {
PopoutType = PopoutType.Undetermined; WindowType = WindowType.Undetermined;
} }
public int ProcessId { get; set; }
public string Title { get; set; }
public IntPtr Handle { get; set; } public IntPtr Handle { get; set; }
public string Title { get; set; }
public string ClassName { get; set; } public string ClassName { get; set; }
public PopoutType PopoutType { get; set; } public WindowType WindowType { get; set; }
}
public enum PopoutType
{
BuiltIn,
Custom,
Undetermined
} }
} }

18
Modules/Enums.cs Normal file
View file

@ -0,0 +1,18 @@
namespace MSFSPopoutPanelManager
{
public enum FlightSimResolution
{
HD,
QHD,
WQHD,
UHD
}
public enum WindowType
{
FlightSimMainWindow,
BuiltIn_Popout,
Custom_Popout,
Undetermined
}
}

172
Modules/FileManager.cs Normal file
View file

@ -0,0 +1,172 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;
namespace MSFSPopoutPanelManager
{
public class FileManager
{
private static string StartupPath;
static FileManager()
{
FileManager.StartupPath = Application.StartupPath;
}
public static List<PlaneProfile> ReadPlaneProfileData()
{
try
{
using (StreamReader reader = new StreamReader(GetFilePathByType(FilePathType.ProfileData) + "planeprofile.json"))
{
return JsonConvert.DeserializeObject<List<PlaneProfile>>(reader.ReadToEnd());
}
}
catch
{
return new List<PlaneProfile>();
}
}
public static UserData ReadUserData()
{
try
{
using (StreamReader reader = new StreamReader(GetFilePathByType(FilePathType.UserData) + "userdata.json"))
{
return JsonConvert.DeserializeObject<UserData>(reader.ReadToEnd());
}
}
catch
{
var userData = new UserData();
return userData;
}
}
public static UserPlaneProfile GetUserPlaneProfile(int profileId)
{
var userData = ReadUserData();
var userPlaneProfile = userData.Profiles.Find(x => x.ProfileId == profileId);
if (userPlaneProfile == null)
{
userPlaneProfile = new UserPlaneProfile();
userPlaneProfile.ProfileId = profileId;
userPlaneProfile.PanelSettings.PanelDestinationList = new List<PanelDestinationInfo>();
userPlaneProfile.PanelSourceCoordinates = new List<PanelSourceCoordinate>();
}
return userPlaneProfile;
}
public static void WriteUserData(UserData userData)
{
using (StreamWriter file = File.CreateText(GetFilePathByType(FilePathType.UserData) + "userdata.json"))
{
JsonSerializer serializer = new JsonSerializer();
serializer.Serialize(file, userData);
}
}
public static List<AnalysisData> ReadAnalysisTemplateData()
{
using (StreamReader reader = new StreamReader(GetFilePathByType(FilePathType.AnalysisData) + "analysisconfig.json"))
{
try
{
return JsonConvert.DeserializeObject<List<AnalysisData>>(reader.ReadToEnd());
}
catch(Exception ex)
{
throw new Exception("The file analysisconfig.json is invalid.");
}
}
}
public static Stream LoadAsStream(FilePathType filePathType, string fileName)
{
try
{
var fullFilePath = GetFilePathByType(filePathType) + fileName;
return new MemoryStream(File.ReadAllBytes(fullFilePath));
}
catch
{
Logger.LogStatus($"Unable to load file {fileName}");
return null;
}
}
public static Stream LoadAsStream(string fullFilePath)
{
return new MemoryStream(File.ReadAllBytes(fullFilePath));
}
public static void SaveFile(FilePathType filePathType, string fileName, MemoryStream memoryStream)
{
var folderPath = GetFilePathByType(filePathType);
var fullFilePath = GetFilePathByType(filePathType) + fileName;
Directory.CreateDirectory(folderPath);
using (var file = new FileStream(fullFilePath, FileMode.Create, FileAccess.Write))
{
memoryStream.WriteTo(file);
}
}
public static List<string> GetFileNames(FilePathType filePathType, string subFolder, string filePrefix)
{
List<string> files = new List<string>();
var folderPath = GetFilePathByType(filePathType);
if (!String.IsNullOrEmpty(subFolder))
folderPath += subFolder + @"\";
string[] fileEntries = Directory.GetFiles(folderPath);
foreach (string fileEntry in fileEntries)
{
var fileName = Path.GetFileName(fileEntry);
if (!String.IsNullOrEmpty(filePrefix))
{
if(fileName.StartsWith(filePrefix))
files.Add(fileEntry);
}
else
files.Add(fileEntry);
}
return files;
}
private static string GetFilePathByType(FilePathType filePathType)
{
switch (filePathType)
{
case FilePathType.PreprocessingData:
return StartupPath + @"\Config\PreprocessingData\";
case FilePathType.AnalysisData:
return StartupPath + @"\Config\AnalysisData\";
case FilePathType.ProfileData:
case FilePathType.UserData:
return StartupPath + @"\Config\";
default:
return StartupPath;
}
}
}
public enum FilePathType
{
PreprocessingData,
AnalysisData,
ProfileData,
UserData,
Default
}
}

54
Modules/ImageAnalysis.cs Normal file
View file

@ -0,0 +1,54 @@
using AForge.Imaging;
using AForge.Imaging.Filters;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Threading;
namespace MSFSPopoutPanelManager
{
public class ImageAnalysis
{
public static Point ExhaustiveTemplateMatchAnalysis(Bitmap sourceImage, Bitmap templateImage, int imageShrinkFactor, float similarityThreshHold)
{
const int MICROSOFT_WINDOWS_HANDLE_FRAME_X_VALUE_ADJUSTMENT = -8;
ExhaustiveTemplateMatching etm = new ExhaustiveTemplateMatching(similarityThreshHold);
TemplateMatch[] templateMatches = etm.ProcessImage(sourceImage, templateImage);
// Highlight the matchings that were found and saved a copy of the highlighted image
if (templateMatches != null && templateMatches.Length > 0)
{
var match = templateMatches.OrderByDescending(x => x.Similarity).First(); // Just look at the first match since only one operation can be accomplished at a time on MSFS side
var x = match.Rectangle.X * imageShrinkFactor;
var y = match.Rectangle.Y * imageShrinkFactor;
//var width = match.Rectangle.Width * imageShrinkFactor;
//var height = match.Rectangle.Height * imageShrinkFactor;
//var centerX = x + width / 2 + MICROSOFT_WINDOWS_HANDLE_FRAME_X_VALUE_ADJUSTMENT;
//var centerY = y + height / 2;
return new Point(x, y);
}
return Point.Empty;
}
public static float ExhaustiveTemplateMatchAnalysisScore(Bitmap sourceImage, Bitmap templateImage, float similarityThreshHold)
{
ExhaustiveTemplateMatching etm = new ExhaustiveTemplateMatching(similarityThreshHold);
TemplateMatch[] templateMatches = etm.ProcessImage(sourceImage, templateImage);
// Highlight the matchings that were found and saved a copy of the highlighted image
if (templateMatches != null && templateMatches.Length > 0)
{
var imageMatched = templateMatches.ToList().Max(x => x.Similarity);
return imageMatched;
}
return 0;
}
}
}

95
Modules/ImageOperation.cs Normal file
View file

@ -0,0 +1,95 @@
using AForge.Imaging;
using AForge.Imaging.Filters;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Threading;
namespace MSFSPopoutPanelManager
{
public class ImageOperation
{
public static byte[] ImageToByte(Bitmap image)
{
ImageConverter converter = new ImageConverter();
return (byte[])converter.ConvertTo(image, typeof(byte[]));
}
public static Bitmap ByteToImage(byte[] bytes)
{
return new Bitmap(new MemoryStream(bytes));
}
public static Bitmap ResizeImage(Bitmap sourceImage, float width, float height)
{
return new ResizeBilinear(Convert.ToInt32(width), Convert.ToInt32(height)).Apply(sourceImage);
}
public static Bitmap CropImage(Bitmap sourceImage, int x, int y, int width, int height)
{
Rectangle crop = new Rectangle(x, y, width, height);
var bmp = new Bitmap(crop.Width, crop.Height);
using (var gr = Graphics.FromImage(bmp))
{
gr.DrawImage(sourceImage, new Rectangle(0, 0, bmp.Width, bmp.Height), crop, GraphicsUnit.Pixel);
}
return bmp;
}
public static Bitmap ConvertToFormat(Bitmap image, PixelFormat format)
{
var copy = new Bitmap(image.Width, image.Height, format);
using (Graphics gr = Graphics.FromImage(copy))
{
gr.DrawImage(image, new Rectangle(0, 0, copy.Width, copy.Height));
}
return copy;
}
public static Bitmap TakeScreenShot(IntPtr windowHandle, bool maximized)
{
if (maximized)
{
const int SW_MAXIMIZE = 3;
PInvoke.ShowWindow(windowHandle, SW_MAXIMIZE);
}
// Set window to foreground so nothing can hide the window
PInvoke.SetForegroundWindow(windowHandle);
Thread.Sleep(500);
var rect = new Rect();
PInvoke.GetWindowRect(windowHandle, out rect);
var bounds = new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);
var bitmap = new Bitmap(bounds.Width, bounds.Height);
using (Graphics g = Graphics.FromImage(bitmap))
{
g.CopyFromScreen(new Point(bounds.Left, bounds.Top), Point.Empty, bounds.Size);
}
return bitmap;
}
public static Bitmap HighLightMatchedPattern(Bitmap sourceImage, List<Rect> rectBoxes)
{
// Highlight the match in the source image
var data = sourceImage.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadWrite, sourceImage.PixelFormat);
foreach (Rect rectBox in rectBoxes)
{
Rectangle rect = new Rectangle(rectBox.Left, rectBox.Top, rectBox.Width, rectBox.Height);
Drawing.Rectangle(data, rect, Color.Red);
}
sourceImage.UnlockBits(data);
sourceImage.Save(@".\debug.png");
return sourceImage;
}
}
}

44
Modules/Logger.cs Normal file
View file

@ -0,0 +1,44 @@
using System;
namespace MSFSPopoutPanelManager
{
public class Logger
{
public static event EventHandler<EventArgs<StatusMessage>> OnStatusLogged;
public static void LogStatus(string message)
{
var statusMessage = new StatusMessage() { Message = message, Priority = StatusPriority.Low };
OnStatusLogged?.Invoke(null, new EventArgs<StatusMessage>(statusMessage));
}
public static void LogStatus(string message, StatusPriority priority)
{
var statusMessage = new StatusMessage() { Message = message, Priority = priority };
OnStatusLogged?.Invoke(null, new EventArgs<StatusMessage>(statusMessage));
}
}
public class EventArgs<T> : EventArgs
{
public T Value { get; private set; }
public EventArgs(T val)
{
Value = val;
}
}
public class StatusMessage
{
public string Message { get; set; }
public StatusPriority Priority { get; set; }
}
public enum StatusPriority
{
High,
Low
}
}

78
Modules/PInvoke.cs Normal file
View file

@ -0,0 +1,78 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace MSFSPopoutPanelManager
{
public class PInvoke
{
[DllImport("user32.dll")]
public static extern bool ClientToScreen(IntPtr hWnd, ref System.Drawing.Point lpPoint);
[DllImport("user32.dll")]
public static extern IntPtr CreateWindowEx(int dwExStyle, string lpClassName, string lpWindowName, int dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DestroyWindow(IntPtr hwnd);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EnumChildWindows(IntPtr window, EnumWindowProc callback, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int GetClassName(IntPtr hWnd, StringBuilder strPtrClassName, Int32 nMaxCount);
[DllImport("user32.dll")]
public static extern bool GetClientRect(IntPtr hWnd, out Rect lpRect);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll")]
public static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
public static extern int GetWindowRect(IntPtr hwnd, out Rect lpRect);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpWindowText, int nMaxCount);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
[DllImport("user32.dll")]
public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo);
[DllImport("user32.dll")]
public static extern void mouse_event(uint dwFlags, int dx, int dy, uint cButtons, uint dwExtraInfo);
[DllImport("user32.dll")]
public static extern bool MoveWindow(IntPtr hWnd, int x, int y, int width, int height, bool repaint);
[DllImport("User32.dll")]
public static extern bool SetCursorPos(int X, int Y);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam);
[DllImport("USER32.dll")]
public static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong);
[DllImport("user32.dll")]
public static extern IntPtr SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int x, int y, int cx, int cy, uint wFlags);
[DllImport("user32.dll")]
public static extern bool SetWindowText(System.IntPtr hwnd, System.String lpString);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
public delegate bool EnumWindowProc(IntPtr hwnd, IntPtr lParam);
}
}

View file

@ -0,0 +1,409 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Text;
using System.Drawing.Imaging;
using System.Threading.Tasks;
namespace MSFSPopoutPanelManager
{
public class PanelAnalysisModule
{
private Form _form;
public PanelAnalysisModule(Form form)
{
_form = form;
}
public void Analyze(WindowProcess simulatorProcess, int profileId, int panelsCount)
{
var panelsToBeIdentified = panelsCount;
// Get all child windows
var processZero = GetProcessZero();
// Move process zero childs back into simulator process
MoveChildWindowsIntoSimulatorPrcess(simulatorProcess, processZero);
if (simulatorProcess.ChildWindows.Count > 0)
{
foreach(var customPopout in simulatorProcess.ChildWindows.FindAll(x => x.WindowType == WindowType.Undetermined))
{
while (panelsToBeIdentified > 0)
{
var coordinate = AnalyzeMergedPopoutWindows(simulatorProcess.Handle, customPopout.Handle);
if (!coordinate.IsEmpty)
SeparateUntitledPanel(customPopout.Handle, coordinate.X, coordinate.Y);
panelsToBeIdentified--;
}
panelsToBeIdentified = panelsCount;
}
}
// Now all newly pop out windows are in process zero, move them into flight simulator process
processZero = GetProcessZero();
MoveChildWindowsIntoSimulatorPrcess(simulatorProcess, processZero);
// Analyze the content of the pop out panels
AnalyzePopoutWindows(simulatorProcess, profileId);
}
private WindowProcess GetProcessZero()
{
// Get process with PID of zero (PID zero launches all the popout windows for MSFS)
var process = Process.GetProcesses().ToList().Find(x => x.Id == 0);
var processZero = new WindowProcess()
{
ProcessId = process.Id,
ProcessName = process.ProcessName,
Handle = process.MainWindowHandle
};
GetChildWindows(processZero);
return processZero;
}
private void GetChildWindows(WindowProcess process)
{
int classNameLength = 256;
var childHandles = GetAllChildHandles(process.Handle);
childHandles.ForEach(childHandle =>
{
StringBuilder className = new StringBuilder(classNameLength);
PInvoke.GetClassName(childHandle, className, classNameLength);
if (className.ToString() == "AceApp")
{
process.ChildWindows.Add(new ChildWindow
{
ClassName = "AceApp",
Handle = childHandle,
Title = GetWindowTitle(childHandle)
});
}
});
}
private List<IntPtr> GetAllChildHandles(IntPtr parent)
{
var childHandles = new List<IntPtr>();
GCHandle gcChildhandlesList = GCHandle.Alloc(childHandles);
IntPtr pointerChildHandlesList = GCHandle.ToIntPtr(gcChildhandlesList);
try
{
PInvoke.EnumWindowProc childProc = new PInvoke.EnumWindowProc(EnumWindow);
PInvoke.EnumChildWindows(parent, childProc, pointerChildHandlesList);
}
finally
{
gcChildhandlesList.Free();
}
return childHandles;
}
private bool EnumWindow(IntPtr hWnd, IntPtr lParam)
{
var gcChildhandlesList = GCHandle.FromIntPtr(lParam);
if (gcChildhandlesList.Target == null)
return false;
var childHandles = gcChildhandlesList.Target as List<IntPtr>;
childHandles.Add(hWnd);
return true;
}
private string GetWindowTitle(IntPtr hWnd)
{
StringBuilder title = new StringBuilder(1024);
PInvoke.GetWindowText(hWnd, title, title.Capacity);
return String.IsNullOrEmpty(title.ToString()) ? null : title.ToString();
}
private void MoveChildWindowsIntoSimulatorPrcess(WindowProcess simulatorProcess, WindowProcess processZero)
{
// The popout windows such as PFD and MFD attached itself to main window for Process ID zero instead of the MSFS process.
// Moving these windows back into MSFS main window
if (processZero != null)
{
// Clean up all existing simulator process child window data
simulatorProcess.ChildWindows.RemoveAll(x => x.WindowType == WindowType.Custom_Popout || x.WindowType == WindowType.BuiltIn_Popout);
foreach (var child in processZero.ChildWindows)
{
int parentProcessId;
PInvoke.GetWindowThreadProcessId(child.Handle, out parentProcessId);
if (simulatorProcess != null && parentProcessId == simulatorProcess.ProcessId && !simulatorProcess.ChildWindows.Exists(x => x.Handle == child.Handle))
{
if (String.IsNullOrEmpty(child.Title))
child.WindowType = WindowType.Undetermined;
else if (child.Title.Contains("(Custom)"))
child.WindowType = WindowType.Custom_Popout;
else if (child.Title.Contains("Microsoft Flight Simulator"))
child.WindowType = WindowType.FlightSimMainWindow;
else if (!String.IsNullOrEmpty(child.Title))
child.WindowType = WindowType.BuiltIn_Popout;
else
child.WindowType = WindowType.Undetermined;
simulatorProcess.ChildWindows.Add(child);
}
}
}
}
public Point AnalyzeMergedPopoutWindows(IntPtr flightSimHandle, IntPtr windowHandle)
{
var resolution = GetWindowResolution(flightSimHandle);
int EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR = 1;
float EXHAUSTIVE_TEMPLATE_MATCHING_SIMILARITY_THRESHOLD = 0.9f;
// This is to speed up pattern matching and still balance accuracy
switch (resolution)
{
case FlightSimResolution.HD:
EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR = 2;
EXHAUSTIVE_TEMPLATE_MATCHING_SIMILARITY_THRESHOLD = 0.88f;
break;
case FlightSimResolution.QHD:
case FlightSimResolution.WQHD:
case FlightSimResolution.UHD:
EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR = 3;
EXHAUSTIVE_TEMPLATE_MATCHING_SIMILARITY_THRESHOLD = 0.85f;
break;
}
var windowResolution = GetWindowResolution(flightSimHandle);
var templateFileName = $"separation_button_{windowResolution}.png";
var source = ImageOperation.ConvertToFormat(ImageOperation.TakeScreenShot(windowHandle, true), PixelFormat.Format24bppRgb);
var template = ImageOperation.ConvertToFormat(new Bitmap(FileManager.LoadAsStream(FilePathType.PreprocessingData, templateFileName)), PixelFormat.Format24bppRgb);
// Get the updated template image ratio based on how the initial window with all the popout panels in it are organized. This is used to resize the template image
var templateImageRatio = GetTemplateToPanelHeightRatio(windowHandle, source, windowResolution, 1);
if (templateImageRatio == -1)
{
return Point.Empty;
}
// Resize the source and template image to speed up exhaustive template matching analysis
var sourceWidth = source.Width / EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR;
var sourceHeight = source.Height / EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR;
var templateWidth = template.Width * templateImageRatio / EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR;
var templateHeight = template.Height * templateImageRatio / EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR;
var resizedSource = ImageOperation.ResizeImage(source, sourceWidth, sourceHeight);
resizedSource = ImageOperation.ConvertToFormat(resizedSource, PixelFormat.Format24bppRgb);
var resizedTemplate = ImageOperation.ResizeImage(template, templateWidth, templateHeight);
var point = ImageAnalysis.ExhaustiveTemplateMatchAnalysis(resizedSource, resizedTemplate, EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR, EXHAUSTIVE_TEMPLATE_MATCHING_SIMILARITY_THRESHOLD);
if (point.IsEmpty)
{
template = ImageOperation.ConvertToFormat(new Bitmap(FileManager.LoadAsStream(FilePathType.PreprocessingData, templateFileName)), PixelFormat.Format24bppRgb);
templateImageRatio = GetTemplateToPanelHeightRatio(windowHandle, source, windowResolution, 2); // maybe there are 2 rows of panels in the merged pop out window
templateWidth = template.Width * templateImageRatio / EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR;
templateHeight = template.Height * templateImageRatio / EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR;
resizedTemplate = ImageOperation.ResizeImage(template, templateWidth, templateHeight);
point = ImageAnalysis.ExhaustiveTemplateMatchAnalysis(resizedSource, resizedTemplate, EXHAUSTIVE_TEMPLATE_MATCHING_SHRINK_FACTOR, EXHAUSTIVE_TEMPLATE_MATCHING_SIMILARITY_THRESHOLD);
}
return point;
}
private FlightSimResolution GetWindowResolution(IntPtr windowHandle)
{
var rect = new Rect();
PInvoke.GetClientRect(windowHandle, out rect);
switch (rect.Right)
{
case 1920:
return FlightSimResolution.HD;
case 2560:
case 3440:
return FlightSimResolution.QHD;
case 3840:
return FlightSimResolution.UHD;
default:
return FlightSimResolution.QHD;
}
}
private float GetTemplateToPanelHeightRatio(IntPtr windowHandle, Bitmap sourceImage, FlightSimResolution windowResolution, int numberOfRows)
{
const int SW_MAXIMIZE = 3;
PInvoke.ShowWindow(windowHandle, SW_MAXIMIZE);
PInvoke.SetForegroundWindow(windowHandle);
Thread.Sleep(500);
Rect rect = new Rect();
PInvoke.GetClientRect(windowHandle, out rect);
// Get a snippet of 1 pixel wide vertical strip of windows. We will choose the strip left of center.
// This is to determine when the actual panel's vertical pixel starts in the window. This will allow accurate sizing of the template image
var clientWindowHeight = rect.Bottom - rect.Top;
var left = Convert.ToInt32((rect.Right - rect.Left) * 0.25); // look at around 25% from the left
var top = sourceImage.Height - clientWindowHeight;
if (top < 0 || left < 0)
return -1;
// Using much faster image LockBits instead of GetPixel method
unsafe
{
var stripData = sourceImage.LockBits(new Rectangle(left, top, 1, clientWindowHeight), ImageLockMode.ReadWrite, sourceImage.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(stripData.PixelFormat) / 8;
int heightInPixels = stripData.Height;
int widthInBytes = stripData.Width * bytesPerPixel;
byte* ptrFirstPixel = (byte*)stripData.Scan0;
// Find the first white pixel and have at least 4 more white pixels in a row (the panel title bar)
const int WHITE_PIXEL_TO_COUNT = 4;
int whitePixelCount = 0;
for (int y = 0; y < heightInPixels; y++)
{
byte* currentLine = ptrFirstPixel + (y * stripData.Stride);
for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
{
int red = currentLine[x + 2];
int green = currentLine[x + 1];
int blue = currentLine[x];
if (red == 255 && green == 255 && blue == 255)
{
whitePixelCount++;
if (whitePixelCount == WHITE_PIXEL_TO_COUNT)
{
sourceImage.UnlockBits(stripData);
var unpopPanelSize = (clientWindowHeight - (y) * 2) / Convert.ToSingle(numberOfRows);
var currentRatio = unpopPanelSize / Convert.ToSingle(clientWindowHeight);
return currentRatio;
}
}
else
{
whitePixelCount = 0;
}
}
}
sourceImage.UnlockBits(stripData);
}
return -1;
}
private void SeparateUntitledPanel(IntPtr windowHandle, int x, int y)
{
const uint MOUSEEVENTF_LEFTDOWN = 0x02;
const uint MOUSEEVENTF_LEFTUP = 0x04;
var point = new Point { X = x, Y = y };
Cursor.Position = new Point(point.X, point.Y);
PInvoke.mouse_event(MOUSEEVENTF_LEFTDOWN, x, y, 0, 0);
Thread.Sleep(200);
PInvoke.mouse_event(MOUSEEVENTF_LEFTUP, x, y, 0, 0);
}
private void AnalyzePopoutWindows(WindowProcess simulatorProcess, int profileId)
{
List<PanelScore> panelScores = new List<PanelScore>();
// Get analysis template data for the profile
var planeProfile = FileManager.ReadPlaneProfileData().Find(x => x.ProfileId == profileId);
var templateData = FileManager.ReadAnalysisTemplateData().Find(x => x.TemplateName == planeProfile.AnalysisTemplateName);
// Load the template images for the selected profile
List<KeyValuePair<string, Bitmap>> templates = new List<KeyValuePair<string, Bitmap>>();
foreach (var template in templateData.Templates)
{
foreach (var imagePath in template.ImagePaths)
{
templates.Add(new KeyValuePair<string, Bitmap>(template.PopoutName, ImageOperation.ConvertToFormat(new Bitmap(FileManager.LoadAsStream(FilePathType.AnalysisData, imagePath)), PixelFormat.Format24bppRgb)));
}
}
var popouts = simulatorProcess.ChildWindows.FindAll(x => x.WindowType == WindowType.Undetermined);
foreach (var popout in popouts)
{
popout.WindowType = WindowType.Custom_Popout;
// Resize all untitled pop out panels to 800x600 and set it to foreground
PInvoke.MoveWindow(popout.Handle, 0, 0, 800, 600, true);
PInvoke.SetForegroundWindow(popout.Handle);
Thread.Sleep(300); // ** this delay is important to allow the window to go into focus before screenshot is taken
var srcImage = ImageOperation.ConvertToFormat(ImageOperation.TakeScreenShot(popout.Handle, false), PixelFormat.Format24bppRgb);
var srcImageBytes = ImageOperation.ImageToByte(srcImage);
Parallel.ForEach(templates, new ParallelOptions { MaxDegreeOfParallelism = Convert.ToInt32(Math.Ceiling((Environment.ProcessorCount * 0.75) * 1.0)) }, template =>
{
var src = ImageOperation.ByteToImage(srcImageBytes);
panelScores.Add(new PanelScore
{
WindowHandle = popout.Handle,
PanelName = template.Key,
Score = ImageAnalysis.ExhaustiveTemplateMatchAnalysisScore(src, template.Value, 0.85f)
});
});
}
// Gets the highest matching score for template matches for each panel
var panels = (from s in panelScores
group s by s.WindowHandle into g
select g.OrderByDescending(z => z.Score).FirstOrDefault()).ToList();
// Set the pop out panel title bar text to identify it
foreach (var panel in panels)
{
var title = $"{panel.PanelName} (Custom)";
simulatorProcess.ChildWindows.Find(x => x.Handle == panel.WindowHandle).Title = title;
PInvoke.SetWindowText(panel.WindowHandle, title);
}
}
private Bitmap CloneImage(Bitmap srcImage)
{
return srcImage.Clone(new Rectangle(0, 0, srcImage.Width, srcImage.Height), PixelFormat.Format24bppRgb);
}
}
public class PanelScore
{
public IntPtr WindowHandle { get; set; }
public string PanelName { get; set; }
public float Score { get; set; }
}
}

View file

@ -0,0 +1,153 @@
using Gma.System.MouseKeyHook;
using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace MSFSPopoutPanelManager
{
public class PanelLocationSelectionModule
{
private IKeyboardMouseEvents _mouseHook;
private int _panelIndex;
private Form _appForm;
public event EventHandler OnLocationListChanged;
public event EventHandler OnSelectionStarted;
public event EventHandler OnSelectionCompleted;
public PanelLocationSelectionModule(Form appForm)
{
_appForm = appForm;
}
public UserPlaneProfile PlaneProfile { get; set; }
public void Start()
{
if (_mouseHook == null)
{
_mouseHook = Hook.GlobalEvents();
_mouseHook.MouseDownExt += HandleMouseHookMouseDownExt;
}
_panelIndex = 1;
PlaneProfile.PanelSourceCoordinates = new List<PanelSourceCoordinate>();
PlaneProfile.PanelSettings = new PanelSettings();
ShowPanelLocationOverlay(true);
OnSelectionStarted?.Invoke(this, null);
UpdatePanelLocationUI();
Logger.LogStatus("Panels selection has started.");
}
public void Reset()
{
PlaneProfile.PanelSourceCoordinates = new List<PanelSourceCoordinate>();
_panelIndex = 1;
UpdatePanelLocationUI();
}
public void DrawPanelLocationOverlay()
{
for (int i = Application.OpenForms.Count - 1; i >= 0; i--)
{
if (Application.OpenForms[i].GetType() == typeof(PopoutCoorOverlayForm))
Application.OpenForms[i].Close();
}
if (PlaneProfile.PanelSourceCoordinates != null && PlaneProfile.PanelSourceCoordinates.Count > 0)
{
foreach (var coor in PlaneProfile.PanelSourceCoordinates)
WindowManager.AddPanelLocationSelectionOverlay(coor.PanelIndex.ToString(), coor.X, coor.Y);
}
}
public void ShowPanelLocationOverlay(bool show)
{
for (int i = 0; i < Application.OpenForms.Count; i++)
{
if (Application.OpenForms[i].GetType() == typeof(PopoutCoorOverlayForm))
Application.OpenForms[i].Visible = show;
}
}
public List<PanelSourceCoordinate> PanelCoordinates
{
get
{
return PlaneProfile.PanelSourceCoordinates;
}
}
private void Stop()
{
if (_mouseHook != null)
{
_mouseHook.MouseDownExt -= HandleMouseHookMouseDownExt;
_mouseHook.Dispose();
_mouseHook = null;
}
OnSelectionCompleted?.Invoke(this, null);
UpdatePanelLocationUI();
}
private void HandleMouseHookMouseDownExt(object sender, MouseEventExtArgs e)
{
if (e.Button == MouseButtons.Left)
{
var ctrlPressed = Control.ModifierKeys.ToString() == "Control";
var shiftPressed = Control.ModifierKeys.ToString() == "Shift";
if (ctrlPressed)
{
Stop();
if (PlaneProfile.PanelSourceCoordinates.Count > 0)
Logger.LogStatus($"Panels selection completed and {PlaneProfile.PanelSourceCoordinates.Count} panel(s) have been selected. Please click 'Analyze' to start popping out these panels.");
else
Logger.LogStatus("Panels selection completed. No panel has been selected.");
// Bring app windows back to top
PInvoke.SetForegroundWindow(_appForm.Handle);
}
else if (shiftPressed && Application.OpenForms.Count > 1)
{
// Remove last drawn overlay and last value
Application.OpenForms[Application.OpenForms.Count - 1].Close();
PlaneProfile.PanelSourceCoordinates.RemoveAt(PlaneProfile.PanelSourceCoordinates.Count - 1);
_panelIndex--;
UpdatePanelLocationUI();
}
else if (!shiftPressed)
{
var minX = _appForm.Location.X;
var minY = _appForm.Location.Y;
var maxX = _appForm.Location.X + _appForm.Width;
var maxY = _appForm.Location.Y + _appForm.Height;
if (e.X < minX || e.X > maxX || e.Y < minY || e.Y > maxY)
{
PlaneProfile.PanelSourceCoordinates.Add(new PanelSourceCoordinate() { PanelIndex = _panelIndex, X = e.X, Y = e.Y });
WindowManager.AddPanelLocationSelectionOverlay(_panelIndex.ToString(), e.X, e.Y);
_panelIndex++;
}
UpdatePanelLocationUI();
}
}
}
public void UpdatePanelLocationUI()
{
OnLocationListChanged?.Invoke(this, null);
DrawPanelLocationOverlay();
}
}
}

261
Modules/PanelManager.cs Normal file
View file

@ -0,0 +1,261 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace MSFSPopoutPanelManager
{
public class PanelManager
{
private Form _appForm;
private const int MSFS_CONNECTION_RETRY_TIMEOUT = 2000;
private System.Timers.Timer _timer;
private WindowProcess _simulatorProcess;
private PanelLocationSelectionModule _panelLocationSelectionModule;
private PanelAnalysisModule _panelAnalysisModule;
private UserPlaneProfile _currentPlaneProfile;
public PanelManager(Form form)
{
_appForm = form;
_panelLocationSelectionModule = new PanelLocationSelectionModule(form);
_panelAnalysisModule = new PanelAnalysisModule(form);
_panelLocationSelectionModule.OnSelectionCompleted += (source, e) => { SavePanelSelectionLocation(); };
}
public event EventHandler OnSimulatorStarted;
public event EventHandler OnPanelSettingsChanged;
public event EventHandler OnAnalysisCompleted;
public PanelLocationSelectionModule PanelLocationSelection
{
get { return _panelLocationSelectionModule; }
}
public UserPlaneProfile CurrentPanelProfile
{
get { return _currentPlaneProfile; }
}
public void CheckSimulatorStarted()
{
// Autoconnect to flight simulator
_timer = new System.Timers.Timer();
_timer.Interval = MSFS_CONNECTION_RETRY_TIMEOUT;
_timer.Enabled = true;
_timer.Elapsed += (source, e) =>
{
foreach (var process in Process.GetProcesses())
{
if (process.ProcessName == "FlightSimulator" && _simulatorProcess == null)
{
_simulatorProcess = new WindowProcess()
{
ProcessId = process.Id,
ProcessName = process.ProcessName,
Handle = process.MainWindowHandle
};
_timer.Enabled = false;
OnSimulatorStarted?.Invoke(this, null);
}
}
};
}
public void PlaneProfileChanged(int profileId, bool showCoordinateOverlay)
{
Logger.LogStatus(String.Empty);
_currentPlaneProfile = FileManager.GetUserPlaneProfile(profileId);
_panelLocationSelectionModule.PlaneProfile = _currentPlaneProfile;
_panelLocationSelectionModule.ShowPanelLocationOverlay(showCoordinateOverlay);
_panelLocationSelectionModule.UpdatePanelLocationUI();
}
public void SetDefaultProfile()
{
var userData = FileManager.ReadUserData();
userData.DefaultProfileId = _currentPlaneProfile.ProfileId;
FileManager.WriteUserData(userData);
var profileName = FileManager.ReadPlaneProfileData().Find(x => x.ProfileId == _currentPlaneProfile.ProfileId).ProfileName;
Logger.LogStatus($"Profile '{profileName}' has been set as default.");
}
public void SavePanelSelectionLocation()
{
var profileId = _currentPlaneProfile.ProfileId;
var userData = FileManager.ReadUserData();
if (userData == null)
userData = new UserData();
if (!userData.Profiles.Exists(x => x.ProfileId == profileId))
{
userData.Profiles.Add(_currentPlaneProfile);
}
else
{
var profileIndex = userData.Profiles.FindIndex(x => x.ProfileId == profileId);
userData.Profiles[profileIndex] = _currentPlaneProfile;
}
FileManager.WriteUserData(userData);
Logger.LogStatus("Panel location coordinates have been saved.");
}
public void Analyze()
{
if (PanelLocationSelection.PanelCoordinates == null || PanelLocationSelection.PanelCoordinates.Count == 0)
{
Logger.LogStatus("No panel locations to be analyze. Please select at least one panel first.");
return;
}
Logger.LogStatus("Panel analysis in progress. Please wait...");
Thread.Sleep(1000); // allow time for the mouse to be stopped moving by the user
PanelLocationSelection.ShowPanelLocationOverlay(false);
WindowManager.ExecutePopout(_simulatorProcess.Handle, PanelLocationSelection.PanelCoordinates);
_panelAnalysisModule.Analyze(_simulatorProcess, _currentPlaneProfile.ProfileId, PanelLocationSelection.PanelCoordinates.Count);
PInvoke.SetForegroundWindow(_appForm.Handle);
// Get the identified panel windows and previously saved panel destination location
List<PanelDestinationInfo> panelDestinationList = new List<PanelDestinationInfo>();
var panels = _simulatorProcess.ChildWindows.FindAll(x => x.WindowType == WindowType.Custom_Popout || x.WindowType == WindowType.BuiltIn_Popout);
var hasExistingData = _currentPlaneProfile.PanelSettings.PanelDestinationList.Count > 0;
foreach (var panel in panels)
{
if (hasExistingData)
{
var index = _currentPlaneProfile.PanelSettings.PanelDestinationList.FindIndex(x => x.PanelName == panel.Title);
_currentPlaneProfile.PanelSettings.PanelDestinationList[index].PanelHandle = panel.Handle;
}
else
{
Rect rect = new Rect();
var window = PInvoke.GetWindowRect(panel.Handle, out rect);
PanelDestinationInfo panelDestinationInfo = new PanelDestinationInfo
{
PanelName = panel.Title,
PanelHandle = panel.Handle,
Left = rect.Left,
Top = rect.Top,
Width = rect.Right - rect.Left,
Height = rect.Bottom - rect.Top
};
_currentPlaneProfile.PanelSettings.PanelDestinationList.Add(panelDestinationInfo);
}
}
OnAnalysisCompleted?.Invoke(this, null);
if (panelDestinationList.Count > 0)
Logger.LogStatus("Analysis has been completed. You may now drag the panels to their desire locations.");
else
Logger.LogStatus("No panel has been identified.");
ApplyPanelSettings();
}
public void ApplyPanelSettings()
{
var panels = _simulatorProcess.ChildWindows.FindAll(x => x.WindowType == WindowType.Custom_Popout || x.WindowType == WindowType.BuiltIn_Popout);
int applyCount = 0;
Parallel.ForEach(panels, panel =>
{
var panelDestinationInfo = _currentPlaneProfile.PanelSettings.PanelDestinationList.Find(x => x.PanelName == panel.Title);
if (panelDestinationInfo != null && panelDestinationInfo.Width != 0 && panelDestinationInfo.Height != 0)
{
if (panelDestinationInfo.Left != 0 && panelDestinationInfo.Top != 0)
{
// Apply locations
PInvoke.MoveWindow(panel.Handle, panelDestinationInfo.Left, panelDestinationInfo.Top, panelDestinationInfo.Width, panelDestinationInfo.Height, true);
applyCount++;
}
// Apply always on top
Thread.Sleep(300);
WindowManager.ApplyAlwaysOnTop(panel.Handle, _currentPlaneProfile.PanelSettings.AlwaysOnTop);
// Apply hide title bar
Thread.Sleep(300);
WindowManager.ApplyHidePanelTitleBar(panel.Handle, _currentPlaneProfile.PanelSettings.HidePanelTitleBar);
}
});
if(applyCount > 0)
Logger.LogStatus("Previously saved panel settings have been applied.");
else if (panels.Count > 0 && applyCount == 0)
Logger.LogStatus("Please move the newly identified panels to their desire locations. Once everything is perfect, click 'Save Settings' and these settings will be used in future flights.");
else
Logger.LogStatus("No panel has been found.");
}
public void SavePanelSettings()
{
var profileId = _currentPlaneProfile.ProfileId;
// Get latest panel destination locations from screen
var panels = _simulatorProcess.ChildWindows.FindAll(x => x.WindowType == WindowType.Custom_Popout || x.WindowType == WindowType.BuiltIn_Popout);
foreach(var panel in panels)
{
Rect rect = new Rect();
var window = PInvoke.GetWindowRect(panel.Handle, out rect);
var panelDestinationInfo = _currentPlaneProfile.PanelSettings.PanelDestinationList.Find(x => x.PanelName == panel.Title);
if (panelDestinationInfo == null)
{
panelDestinationInfo = new PanelDestinationInfo() { PanelName = panel.Title };
_currentPlaneProfile.PanelSettings.PanelDestinationList.Add(panelDestinationInfo);
}
panelDestinationInfo.Left = rect.Left;
panelDestinationInfo.Top = rect.Top;
panelDestinationInfo.Width = rect.Right - rect.Left;
panelDestinationInfo.Height = rect.Bottom - rect.Top;
}
var userData = FileManager.ReadUserData();
if (!userData.Profiles.Exists(x => x.ProfileId == _currentPlaneProfile.ProfileId))
userData.Profiles.Add(_currentPlaneProfile);
else
{
var profileIndex = userData.Profiles.FindIndex(x => x.ProfileId == _currentPlaneProfile.ProfileId);
userData.Profiles[profileIndex] = _currentPlaneProfile;
}
FileManager.WriteUserData(userData);
OnPanelSettingsChanged?.Invoke(this, null);
Logger.LogStatus("Panel settings have been saved.");
}
public void UpdatePanelLocationUI()
{
_panelLocationSelectionModule.UpdatePanelLocationUI();
}
}
}

29
Modules/PlaneProfile.cs Normal file
View file

@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace MSFSPopoutPanelManager
{
public class PlaneProfile
{
public int ProfileId { get; set; }
public string ProfileName { get; set; }
public string AnalysisTemplateName { get; set; }
}
public class AnalysisData
{
public string TemplateName { get; set; }
public List<Template> Templates { get; set; }
}
public class Template
{
public int PopoutId { get; set; }
public string PopoutName { get; set; }
public List<string> ImagePaths { get; set; }
}
}

37
Modules/Structs.cs Normal file
View file

@ -0,0 +1,37 @@
using System.Runtime.InteropServices;
namespace MSFSPopoutPanelManager
{
[StructLayout(LayoutKind.Sequential)]
public struct Rect
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public int Width
{
get
{
return Right - Left;
}
}
public int Height
{
get
{
return Bottom - Top;
}
}
}
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
}

75
Modules/UserData.cs Normal file
View file

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MSFSPopoutPanelManager
{
public class UserData
{
public UserData()
{
Profiles = new List<UserPlaneProfile>();
DefaultProfileId = 1;
}
public List<UserPlaneProfile> Profiles { get; set; }
public int DefaultProfileId { get; set; }
}
public class UserPlaneProfile
{
public UserPlaneProfile()
{
PanelSourceCoordinates = new List<PanelSourceCoordinate>();
PanelSettings = new PanelSettings();
}
public int ProfileId { get; set; }
public PanelSettings PanelSettings { get; set; }
public List<PanelSourceCoordinate> PanelSourceCoordinates;
}
public class PanelSourceCoordinate
{
public int PanelIndex { get; set; }
public int X { get; set; }
public int Y { get; set; }
}
public class PanelSettings
{
public PanelSettings()
{
PanelDestinationList = new List<PanelDestinationInfo>();
AlwaysOnTop = false;
HidePanelTitleBar = false;
}
public List<PanelDestinationInfo> PanelDestinationList { get; set; }
public bool AlwaysOnTop { get; set; }
public bool HidePanelTitleBar { get; set; }
}
public class PanelDestinationInfo
{
public string PanelName { get; set; }
[JsonIgnore]
public IntPtr PanelHandle { get; set; }
public int Top { get; set; }
public int Left { get; set; }
public int Width { get; set; }
public int Height { get; set; }
}
}

105
Modules/WindowManager.cs Normal file
View file

@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
namespace MSFSPopoutPanelManager
{
public class WindowManager
{
private const int SWP_NOMOVE = 0x0002;
private const int SWP_NOSIZE = 0x0001;
private const int SWP_ALWAYS_ON_TOP = SWP_NOMOVE | SWP_NOSIZE;
private const int GWL_STYLE = -16;
private const int WS_SIZEBOX = 0x00040000;
private const int WS_BORDER = 0x00800000;
private const int WS_DLGFRAME = 0x00400000;
private const int WS_CAPTION = WS_BORDER | WS_DLGFRAME;
public static void AddPanelLocationSelectionOverlay(string text, int x, int y)
{
PopoutCoorOverlayForm frm = new PopoutCoorOverlayForm();
frm.Location = new Point(x - frm.Width / 2, y - frm.Height / 2);
frm.StartPosition = FormStartPosition.Manual;
((Label)frm.Controls.Find("lblPanelIndex", true)[0]).Text = text;
frm.Show();
}
private static IntPtr CreateLParam(int LoWord, int HiWord)
{
return (IntPtr)((HiWord << 16) | (LoWord & 0xffff));
}
public static void ExecutePopout(IntPtr simulatorHandle, List<PanelSourceCoordinate> screenCoordinates)
{
PInvoke.SetForegroundWindow(simulatorHandle);
const uint MOUSEEVENTF_LEFTDOWN = 0x02;
const uint MOUSEEVENTF_LEFTUP = 0x04;
const uint KEYEVENTF_EXTENDEDKEY = 0x01;
const uint KEYEVENTF_KEYUP = 0x2;
const uint VK_RMENU = 0xA5; // Right Alternate key
//const int WM_LBUTTONDOWN = 0x201;
//const int WM_LBUTTONUP = 0x202;
foreach (var coor in screenCoordinates)
{
//PInvoke.keybd_event(Convert.ToByte(VK_RMENU), 0, KEYEVENTF_EXTENDEDKEY, 0);
//PInvoke.SendMessage(simulatorHandle, WM_LBUTTONDOWN, 1, CreateLParam(coor.X, coor.Y));
//Thread.Sleep(200);
//PInvoke.SendMessage(simulatorHandle, WM_LBUTTONUP, 0, CreateLParam(coor.X, coor.Y));
//PInvoke.keybd_event(Convert.ToByte(VK_RMENU), 0, KEYEVENTF_KEYUP, 0);
// Move the cursor to the flight simulator screen then move the cursor into position
PInvoke.SetCursorPos(0, 0);
PInvoke.SetCursorPos(coor.X, coor.Y);
// Wait for mouse to get into position
Thread.Sleep(1000);
// Force a left click first
PInvoke.mouse_event(MOUSEEVENTF_LEFTDOWN, coor.X, coor.Y, 0, 0);
Thread.Sleep(200);
PInvoke.mouse_event(MOUSEEVENTF_LEFTUP, coor.X, coor.Y, 0, 0);
Thread.Sleep(300);
// Pop out the screen by Alt Left click
PInvoke.keybd_event(Convert.ToByte(VK_RMENU), 0, KEYEVENTF_EXTENDEDKEY, 0);
PInvoke.mouse_event(MOUSEEVENTF_LEFTDOWN, coor.X, coor.Y, 0, 0);
Thread.Sleep(200);
PInvoke.mouse_event(MOUSEEVENTF_LEFTUP, coor.X, coor.Y, 0, 0);
PInvoke.keybd_event(Convert.ToByte(VK_RMENU), 0, KEYEVENTF_KEYUP, 0);
}
}
public static void ApplyHidePanelTitleBar(IntPtr handle, bool hideTitleBar)
{
var currentStyle = PInvoke.GetWindowLong(handle, GWL_STYLE).ToInt64();
if (hideTitleBar)
PInvoke.SetWindowLong(handle, GWL_STYLE, (uint)(currentStyle & ~(WS_CAPTION | WS_SIZEBOX)));
else
PInvoke.SetWindowLong(handle, GWL_STYLE, (uint)(currentStyle | (WS_CAPTION | WS_SIZEBOX)));
}
public static void ApplyAlwaysOnTop(IntPtr handle, bool alwaysOnTop)
{
if (alwaysOnTop)
{
Rect rect = new Rect();
PInvoke.GetWindowRect(handle, out rect);
PInvoke.SetWindowPos(handle, -1, rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top, SWP_ALWAYS_ON_TOP);
}
else
{
Rect rect = new Rect();
PInvoke.GetWindowRect(handle, out rect);
PInvoke.SetWindowPos(handle, -2, rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top, 0);
}
}
}
}

View file

@ -3,22 +3,19 @@ using System.Collections.Generic;
namespace MSFSPopoutPanelManager namespace MSFSPopoutPanelManager
{ {
public class MainWindow public class WindowProcess
{ {
public MainWindow() public WindowProcess()
{ {
ChildWindowsData = new List<ChildWindow>(); ChildWindows = new List<ChildWindow>();
} }
public int ProcessId { get; set; } public int ProcessId { get; set; }
public string ProcessName { get; set; } public string ProcessName { get; set; }
public string Title { get; set; }
public IntPtr Handle { get; set; } public IntPtr Handle { get; set; }
public List<ChildWindow> ChildWindowsData { get; set; } public List<ChildWindow> ChildWindows { get; set; }
} }
} }

View file

@ -1,25 +0,0 @@
using System.Collections.Generic;
namespace MSFSPopoutPanelManager
{
public class OcrEvalData
{
public string Profile { get; set; }
public bool DefaultProfile { get; set; }
/// <summary>
/// Scale the image so OCR can better recognize text
/// </summary>
public double OCRImageScale { get; set; }
public List<PopoutEvalData> EvalData { get; set; }
}
public class PopoutEvalData
{
public string PopoutName { get; set; }
public List<string> Data { get; set; }
}
}

View file

@ -14,7 +14,7 @@ namespace MSFSPopoutPanelManager
Application.SetHighDpiMode(HighDpiMode.SystemAware); Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles(); Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false); Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm()); Application.Run(new StartupForm());
} }
} }
} }

63
Properties/Resources.Designer.cs generated Normal file
View file

@ -0,0 +1,63 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace MSFSPopoutPanelManager.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MSFSPopoutPanelManager.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}

120
Properties/Resources.resx Normal file
View file

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

128
README.md
View file

@ -1,85 +1,91 @@
# MSFS Pop Out Panel Manager # MSFS Pop Out Panel Manager
MSFS Pop Out Panel Manager is a utility application for MSFS 2020 which helps save and position pop out panels such as PDF and MFD to be used by applications such as Sim Innovations Air Manager's overlay. This is a **proof of concept** application since I do not use Air Manager so I'm kind of guessing how it works. I kept reading messages on flightsimulator.com that flightsimers would like a utility like this so as a coding exercise I just created it. I welcome any feedback to help improve the accuracy and usefulness of this utility. You are welcome to take a copy of this code to further enhance it and use within anything you created. MSFS Pop Out Panel Manager is an application for MSFS 2020 which helps pop out, save and re-position pop out panels such as PDF and MFD to be used by applications such as Sim Innovations Air Manager's overlay. This is a **PROOF OF CONCEPT** application (to also be used by experimental feature in my other github development project - MSFS Touch Panel). I welcome any feedback to help improve the accuracy and usefulness of this application. You are welcome to take a copy of this code to further enhance it and use within anything you created. But please abide by licensing and make it open source:)
## Pop Out Panel Positioning Annoyance ## Pop Out Panel Positioning Annoyance
In MSFS, by holding **Right-Alt** + **Left Clicking** some instrumentation panels, these panels will pop out as floating windows that can be moved to a different monitor. But this needs to be done every time you start a new flight. Predefined toolbar menu windows such as ATC, Checklist, VFR Map can also be popped out. For these MSFS predefined toolbar menu windows, their positions can be saved easily and reposition at the start of each new flight using 3rd party windows positioning tool because these windows have a **title** for the pop out. But panels such as PFD and MFD on G1000 or the FMS panel on G3000 or panels on A320/CJ4 do not have window title. This makes remembering their last used position more difficult and it seems very annoying to resize and readjust their positions to be used by Air Manager or other overlay tool on each new flight. In MSFS, by holding **RIGHT ALT** + **LEFT CLICKING** some instrumentation panels, these panels will pop out as floating windows that can be moved to a different monitor. But this needs to be done every time you start a new flight, ALT-RIGHT clicking, split out child windows, move these windows to final location, rinse and repeat. For predefined toolbar menu windows such as ATC, Checklist, VFR Map, their positions can be saved easily and reposition at the start of each new flight using 3rd party windows positioning tool because these windows have a **TITLE** in the title bar when they are popped out. But panels such as PFD and MFD on G1000 or the multi control panel on G3000 or panels on A320/CJ4 do not have window title. This makes remembering their last used position more difficult and it seems very annoying to resize and readjust their positions to be used by Air Manager or other overlay tool on each new flight.
## Concept to Fix this Annoyance ## Concepts of the Application
The concept to determine the untitled panels to be reposition is pretty straight forward: What if you can do the setup once by defining on screen where the pop out windows will be, click a button, and the application will figure out everything for you. Then you just need to drag these pop out windows to their final desire location (again doing it once) on screen and it will save their final positions. Next time when you start a flight, you just re-position your initial game screen pop out coordinates and click a button and Viola, everything will be done for you. No ALT-Right clicking, no splitting windows manually (imaging you just pop out 7 windows for A32NX), no re-positioning windows to their final destination on screen. The pop outs will even have a title for you to use if you prefer using PowerToys fancy zone to position the windows yourself.
- User will pop out the individual untitled panel.
- This utility will take a screenshot of each panel. Version 1.0 of this application uses OCR (text recognition) technology to try to determine what pop out window is what. It works for some windows but not others. As one of the user in Flightsimulator.com forum asked, can the application pop out windows from A32NX where some of the windows have no text. Also, this version does not do auto pop out. So I scraped that idea of improving OCR and think about using something else instead.
- The screenshot images will be fed into OCR using [Tesseract](https://github.com/charlesw/tesseract/) package.
- The OCRed text (mostly 'gibberish' by the way) will be compared to a predefined set of keywords that are defined by the user. These comparison text can be customized and additional panel types and profiles can be [added](#profile-and-ocr-data-file-definition) for various plane configurations. Version 2.0 uses image recognition and it seems to do a pretty decent job in popping out panels and figure out what window is what. I setup very simple image recognition data (aka. mostly non-scale image match for now) for most planes in MSFS and tested it mainly on 3440x1440 and 1920x1080 resolution. Regular 1440p should work as expected. 4K resolution may or may not work since I don't have a 4K screen to test but I scaled up some of the image recognition data for 4K so it may work! The image recognition uses a confident score to determine what the pop out window most likely to be.
- By being able to identify the untitled panels, this solves the problem of not able to easily reposition these panel in subsequent flight.
The v2.0 application concept is as follow:
1. First allow user to define where all the 'ALT-Right Click' panels on the screen will be for a given plane profile and save these information.
2. Using image recognition, the application figures out how to pop out these panels and also do the clicking for you.
3. Once the pop out panels are split out, the application will start image recognition to determine the type and content for each panel. It also add title bar to these pop out window. Built-in toolbar panel such as ATC, VFR Map will work too.
4. After pop out panels are analyzed, the user will move these panels to their desire locations. An UI will also allow the user to resize and position these windows to pixel perfect location.
5. User will then save these data and start flying.
6. On subsequent flight, user will just need to reposition the definition of the 'ALT-Right Click' panels and the application will do the rest.
## How to Use? ## How to Use?
1. User starts the application and it will automatic connect when MSFS starts. 1. Start the application **MSFSPopoutPanelManager.exe** and it will automatically connect when MSFS starts.
2. User pops out the **Individual** untitled panels such as MFD or PFD on G1000 in MSFS. **Please make sure the pop out panels are not inactive/blank.** 2. Once the game starts and you're at the beginning of flight, first select a plane profile (for example A32NX by FlybyWire)
3. **Important!** User choose the desired profile. 3. You then click "START PANEL SELECTION" to define where the pop out panels will be using LEFT CLICK and CTRL-LEFT CLICK when done to save these information.
4. User selects **Analyze** for the panels that were popped out.
5. Once analysis is completed, user can positions these untitled panels in addition to any predefined toolbar menu panels to the desired location on the screen. You will notice now the "untitled" panels will have new captions.
6. User select **Save Settings** to save the position of these panels. If desire, user can change addition settings of 'Hide Panel Title Bar' and 'Always on Top'. When everything is perfect, user selects **Save Settings** to save all open panels coordinates and settings for this particular profile.
7. In subsequent flight, user pops out some or all of the untitled and/or predefined panels again.
8. User selects **Analyze** then selects **Apply Settings**.
9. These panels will then automatically reposition themselves to their saved positions.
## User Interface
<p align="center"> <p align="center">
<img src="images/ui-screenshot.png" hspace="10"/> <img src="images/doc/screenshot2.png" width="1000" hspace="10"/>
</p> </p>
## OCR Debug Information 4. Make sure all panels are ON and active. The application's image recognition engine will not be able to analyze and distinguish blank panel screens.
All untitled panels OCR extracted text will be represented here by individual tab. When an untitled panel failed analysis, a tab control marked **Failed Analysis - xxxx **will be shown. You can use these extracted text as basis to adjust the OCR data definition file. 5. Now, click "ANALYZE". **!!!IMPORTANT, please make sure there are no other window obstructing pop out coordinates (where the numbers with circle are). Also, please DO NOT move your mouse during this time since the application is simulating mouse movements.** At last, please be patient (even using multi-thread, the execution of mouse movement will still take some time). The application will start popping out windows one by one and you will see a lot of windows movement on the screen. If something goes wrong, just close all the pop outs and try do "ANALYZE" again.
## OCR Concept / Further Enhancement <p align="center">
<img src="images/doc/seperation_analysis.png" width="1000" hspace="10"/>
</p>
- Currently, Tesseract OCR recognition is not very accurate for this use case but is good enough! 6. Once analysis is completed, you will see a list of panels that have successfully processed by the application.
- Screenshot images text recognition can be [customized](#profile-and-ocr-data-file-definition) by configuration file for better accuracy.
- To enhance the initial accuracy of Tessearat OCR, the screenshot has its color inverted, then sharpened, then grayscale before text recognition occurs.
- I only used the default Tessearact OCR function. There are probably better way to improve the accuracy with OCR advanced features and more preprocessing of the images.
- For panels that have no obvious text such as some of the pop out gauges on A320, currently these panels cannot be recognized. Maybe for future enhancement, using color mapping or snippet of screenshot to do the comparison.
## Profile and OCR Data File Definition <p align="center">
The file definition [config/ocrdata.json](config/ocrdata.json) is pretty self-explanatory. This file is used to define the plane profile for selection in the UI and to define the keywords to be compared on each panel. You can add additional profiles to the file. Below is a sample profile for G1000 that has pop out PFD and a MFD panel: <img src="images/doc/screenshot6.png" width="1000" hspace="10"/>
``` </p>
[
// G1000 7. Please go ahead and drag the pop out panels into their final positions. You can also directly type in coordinates for these panels to move and resize them and then click "APPLY SETTINGS". Please click "SAVE SETTINGS" to update the grid data if you use the mouse to drag the panels. Here is an example when panels are dragged to a secondary monitor and click "SAVE SETTINGS".
{
"profile": "G1000", <p align="center">
"defaultProfile": "true", <img src="images/doc/screenshot7.png" width="1000" hspace="10"/>
"ocrImageScale": 1.0, </p>
"evalData": [
{ 7. You can also select "Panels always on top" and/or "Hide panel title bar" if desire. !! IMPORTANT, panels without title bar cannot be moved. Please just unchecked "Hide panel title bar" then "Apply Settings" to get the panel's title bar back to move them.
"popoutName": "PFD",
"data": [ "PFD", "XPDR", "Nearest", "MAP/HSI", "Ident", "Tmr/Ref" ] 8. Once all the panels are at their final positions, click "SAVE SETTINGS" again and you're done. You can now close or minimize the application.
},
{ 9. On subsequent flight, just reposition the pop out position for the selected plane profile and click "ANALYZE" and see the magic happens. Screenshots below used FBW A32NX as example since some of the pop outs were hidden when the flight starts.
"popoutName": "MFD",
"data": [ "FUEL", "QTY", "GAL", "FFLOW GPH", "OIL", "PRESS", "ELECTRICAL", "BATT", "VOLTS", "Navigation Map" ] <p align="center">
} <img src="images/doc/screenshot3.png" width="1000" hspace="10"/>
] </p>
}
] Move your screen down a little bit by holding Right-Click in flight simulator until everything lines up. You can then click "ANALYZE".
```
- ***profile*** - name of profile for selection in the UI. <p align="center">
- ***defaultProfile*** - set as default profile to be selected on the Profile dropdown in the UI. <img src="images/doc/screenshot4.png" width="1000" hspace="10"/>
- ***ocrImageScale*** - adjust the screenshot image scale to enhance OCR accuracy (can be bigger or smaller and can be different for each profile). </p>
- ***evalData*** - a list of panels for this profile and the associated keyword comparison data
- ***popoutName*** - name for the untitled panel, you can name it anything you want but must be unique for the profile. 10. Since the initial pop out positions may be different from plane to plane even when they are using the same instrumentation system, you can easily add new profiles in a configuration file and points it to the same analysisTemplateName. You can edit the configuration file [planeprofile.json](Config/planeprofile.json) in the **config** folder of the application to add additional profile. Look for profileId 1 and 2 in the file as example where both Cessna 172 and DA62 both uses Working Title G1000 NXi.
- ***data*** - the keyword list, add to or remove from this list to improve matching accuracy. The string comparison is **case insensitive**.
## Image Recognition Concept and Application Configurability
To-Do
** **evalData** comparison is executed in listed order. So if the same keyword appears in multiple pop out definitions, the first match wins. So please try to choose keywords that are unique to the panel. You can use [OCR debugger information](#user-interface) in the UI to finetune the text that are being extracted from the screenshot image.
## Common Problem Resolution ## Common Problem Resolution
- Failed Analysis - Make sure to select correct/desire profile before clicking Analyze. You can manually adjust the size of the panel and try to analyze it again. - Unable to pop out windows correctly - the predefined pop out panel coordinate may not line up correctly or movement of mouse is interfering with pop out execution. Please try to reposition the screen into pop out coordinates. Or you can close and restart the application, close all the opened pop outs, and try the analysis again.
- Failed Analysis - Experiment with ocrImageScale and keyword list in ocrdata definition file. - Pop out windows are not recognized correctly - it is the current limitation of current implementation of image recognition algorithm. More sophisticated image recognition such as [SIFT](https://en.wikipedia.org/wiki/Scale-invariant_feature_transform) will be needed. This is to-do item on my list for future version of app. Also, the panel screen maybe blank which causes the image recognition engine to fail.
- Night time or different world location causes image recognition to fail - application has builtin redundancy image recognition data for this purpose (such as MFD recognition for G1000 where 75% of the screen is a map). But I may not have anticipate all the use cases yet. Please provide feedback and it will help me to improve the image recognition engine.
- Running on non-native monitor resolution does not work - for example running 1080p window resolution on 1440p monitor will not work because of image scaling and calculation issue. But in-game resolution scaling will not get affected. This issue can only be fixed when using scale invariant algorithm or more advanced algorithm.
## Author ## Author
Stanley Kwok Stanley Kwok
[hawkeyesk@outlook.com](mailto:hawkeyesk@outlook.com) [hawkeyesk@outlook.com](mailto:hawkeyesk@outlook.com)
## Credits ## Credits
[Charles Weld Tesseract](https://github.com/charlesw/tesseract/) .NET wrapper for Tesseract OCR package. [Tesseract](https://github.com/charlesw/tesseract/) by Charles Weld - .NET wrapper for Tesseract OCR package. For version 1.x of application.
[SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) 2D graphics API package. [AForge.NET](http://www.aforgenet.com/framework/) Image recognition library.
[DarkUI](http://www.darkui.com/) by Robin Perria

10
Rect.cs
View file

@ -1,10 +0,0 @@
namespace MSFSPopoutPanelManager
{
public struct Rect
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
}

78
UI/PopoutCoorOverlayForm.Designer.cs generated Normal file
View file

@ -0,0 +1,78 @@

namespace MSFSPopoutPanelManager
{
partial class PopoutCoorOverlayForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(PopoutCoorOverlayForm));
this.lblPanelIndex = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// lblPanelIndex
//
this.lblPanelIndex.BackColor = System.Drawing.Color.Transparent;
this.lblPanelIndex.CausesValidation = false;
this.lblPanelIndex.Font = new System.Drawing.Font("Segoe UI", 24F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.lblPanelIndex.ForeColor = System.Drawing.Color.WhiteSmoke;
this.lblPanelIndex.Image = ((System.Drawing.Image)(resources.GetObject("lblPanelIndex.Image")));
this.lblPanelIndex.Location = new System.Drawing.Point(0, 0);
this.lblPanelIndex.Margin = new System.Windows.Forms.Padding(0);
this.lblPanelIndex.Name = "lblPanelIndex";
this.lblPanelIndex.Size = new System.Drawing.Size(62, 44);
this.lblPanelIndex.TabIndex = 0;
this.lblPanelIndex.Text = "1";
this.lblPanelIndex.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
//
// PopoutCoorOverlayForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CausesValidation = false;
this.ClientSize = new System.Drawing.Size(62, 44);
this.ControlBox = false;
this.Controls.Add(this.lblPanelIndex);
this.DoubleBuffered = true;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "PopoutCoorOverlayForm";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide;
this.Text = "PopoutCoorOverlay";
this.TopMost = true;
this.TransparencyKey = System.Drawing.SystemColors.Control;
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Label lblPanelIndex;
}
}

View file

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace MSFSPopoutPanelManager
{
public partial class PopoutCoorOverlayForm : Form
{
public PopoutCoorOverlayForm()
{
InitializeComponent();
}
}
}

View file

@ -0,0 +1,81 @@
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="lblPanelIndex.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAAD4AAAAsCAYAAAA93wvUAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAACxIAAAsSAdLdfvwAAAMkSURBVGhD5drNrgxBGMbxQ7Cw9LETa86ORBDchs8tFtwA
V8ANCNYkLGyESAg3wZoFZ4Vj5zsynn8yb3JSefp0V3X19NBv8ksmb71TVceM7qqpXpnNZpNkk1Ngk1Ng
kxVslaNyTR7Ja1mXH3O8JkcbNdTyHtfXIGyyh31yQ95LbvAe3ksfru+qbLLAbrklPyXirdyWC3JYqNkx
x2tytFHzTiLog76ocWNVYZOZTssnIf7IQzkurrbJFjkpD4Q+CPo8I66+N5vsaJvwaUW8klVxtTnog74i
GIOxXG0xm+xgpzwR4rtcFVfXxxWhb4KxGNPVFbHJFvzrPxbis3BFdnU10DdjEIxZ7ZO3yRZ3hPgoB8TV
1MQYjEUwtqvJZpOb4GJDfJMhP+kUY30V4py4miw22YDbS1y9L4urGRJjEix+9oqr6cwmG8QV/IW49kWI
q33vr7xNGvvll/yWg/PcGBibOTAX5uRqOrFJg6UkcU9c+yLdF4I5ufZObDLB5uGDECfE1SzSKSGYU/HG
xiYTXFEJ1tMsLV3NIjGHWNsX31lsMnFdiGr30ApiLcGW1rW3sskEe2aCnZRrHwNzIZiba29lk4k3QhwS
1z4G5kIwN9feyiYTsVYedH+cibkQzM21t7LJRPy4wA8Irn0MzIVgbq69lU0mJvuHL+NXfZcQg37VJ3tx
m+ztLBYw7M5c+xhipzjoAuaYEJNbsi7bJoU5EINvUrBM21J+eycG35aCYx3umcvwQwQHDsyl11GTTTbg
WId4Lq59EV4K0ftCa5MNWMDEYuaSuJohMSbBHHovpmxyE2eF4KfesX5ePi+uJotNthjzQOGuuJpsNtmC
DcLGI6Qj4upqoO/478WY1TZKNtkBB3jPhOBg76K4uj7okxMb4qmMfmgYtkt87QmuuDWOibllxdWbYAzG
crXFbDITF7yNDwawwCh5MIAVWfpgQJVzMscmC+wR7q3xowURj4JwFWYbyS2ITw68JkcbNemjIOTo041V
hU32wGrqpqxJbvAe3vtPPfyTYvPAvZctLXtmHu36Ipx5gdfxuBc11BZvOErY5P9vtvIXQ8rHsl8eoTUA
AAAASUVORK5CYII=
</value>
</data>
</root>

209
UI/StartupForm.Designer.cs generated Normal file
View file

@ -0,0 +1,209 @@

namespace MSFSPopoutPanelManager
{
partial class StartupForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(StartupForm));
this.panelSteps = new System.Windows.Forms.Panel();
this.linkLabel1 = new System.Windows.Forms.LinkLabel();
this.labelMsfsRunning = new System.Windows.Forms.Label();
this.panelStatus = new System.Windows.Forms.Panel();
this.darkLabel3 = new DarkUI.Controls.DarkLabel();
this.txtBoxStatus = new DarkUI.Controls.DarkTextBox();
this.notifyIcon1 = new System.Windows.Forms.NotifyIcon(this.components);
this.darkLabel1 = new DarkUI.Controls.DarkLabel();
this.darkLabel2 = new DarkUI.Controls.DarkLabel();
this.checkBoxMinimizeToTray = new DarkUI.Controls.DarkCheckBox();
this.lblVersion = new DarkUI.Controls.DarkLabel();
this.panelStatus.SuspendLayout();
this.SuspendLayout();
//
// panelSteps
//
this.panelSteps.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(61)))), ((int)(((byte)(101)))), ((int)(((byte)(171)))));
this.panelSteps.Location = new System.Drawing.Point(0, 64);
this.panelSteps.Name = "panelSteps";
this.panelSteps.Size = new System.Drawing.Size(862, 405);
this.panelSteps.TabIndex = 0;
//
// linkLabel1
//
this.linkLabel1.AutoSize = true;
this.linkLabel1.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.linkLabel1.LinkColor = System.Drawing.Color.FromArgb(((int)(((byte)(192)))), ((int)(((byte)(255)))), ((int)(((byte)(255)))));
this.linkLabel1.Location = new System.Drawing.Point(344, 35);
this.linkLabel1.Name = "linkLabel1";
this.linkLabel1.Size = new System.Drawing.Size(41, 21);
this.linkLabel1.TabIndex = 1;
this.linkLabel1.TabStop = true;
this.linkLabel1.Text = "here";
this.linkLabel1.VisitedLinkColor = System.Drawing.Color.FromArgb(((int)(((byte)(128)))), ((int)(((byte)(255)))), ((int)(((byte)(255)))));
this.linkLabel1.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkLabel1_LinkClicked);
//
// labelMsfsRunning
//
this.labelMsfsRunning.AutoSize = true;
this.labelMsfsRunning.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.labelMsfsRunning.ForeColor = System.Drawing.Color.Red;
this.labelMsfsRunning.Location = new System.Drawing.Point(704, 553);
this.labelMsfsRunning.Name = "labelMsfsRunning";
this.labelMsfsRunning.RightToLeft = System.Windows.Forms.RightToLeft.Yes;
this.labelMsfsRunning.Size = new System.Drawing.Size(143, 20);
this.labelMsfsRunning.TabIndex = 10;
this.labelMsfsRunning.Text = "MSFS is not Running";
this.labelMsfsRunning.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// panelStatus
//
this.panelStatus.Controls.Add(this.darkLabel3);
this.panelStatus.Controls.Add(this.txtBoxStatus);
this.panelStatus.Enabled = false;
this.panelStatus.Location = new System.Drawing.Point(0, 469);
this.panelStatus.Name = "panelStatus";
this.panelStatus.Size = new System.Drawing.Size(860, 74);
this.panelStatus.TabIndex = 20;
//
// darkLabel3
//
this.darkLabel3.AutoSize = true;
this.darkLabel3.Font = new System.Drawing.Font("Segoe UI", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.darkLabel3.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(220)))), ((int)(((byte)(220)))), ((int)(((byte)(220)))));
this.darkLabel3.Location = new System.Drawing.Point(12, 16);
this.darkLabel3.Name = "darkLabel3";
this.darkLabel3.Size = new System.Drawing.Size(49, 20);
this.darkLabel3.TabIndex = 24;
this.darkLabel3.Text = "Status";
//
// txtBoxStatus
//
this.txtBoxStatus.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(69)))), ((int)(((byte)(73)))), ((int)(((byte)(74)))));
this.txtBoxStatus.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.txtBoxStatus.Font = new System.Drawing.Font("Segoe UI", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.txtBoxStatus.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(220)))), ((int)(((byte)(220)))), ((int)(((byte)(220)))));
this.txtBoxStatus.Location = new System.Drawing.Point(67, 16);
this.txtBoxStatus.Multiline = true;
this.txtBoxStatus.Name = "txtBoxStatus";
this.txtBoxStatus.ReadOnly = true;
this.txtBoxStatus.Size = new System.Drawing.Size(780, 46);
this.txtBoxStatus.TabIndex = 23;
//
// notifyIcon1
//
this.notifyIcon1.Icon = ((System.Drawing.Icon)(resources.GetObject("notifyIcon1.Icon")));
this.notifyIcon1.Text = "MSFS 2020 Pop Out Panel Manager";
this.notifyIcon1.Visible = true;
this.notifyIcon1.DoubleClick += new System.EventHandler(this.notifyIcon1_DoubleClick);
//
// darkLabel1
//
this.darkLabel1.AutoSize = true;
this.darkLabel1.Font = new System.Drawing.Font("Segoe UI", 14.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
this.darkLabel1.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(220)))), ((int)(((byte)(220)))), ((int)(((byte)(220)))));
this.darkLabel1.Location = new System.Drawing.Point(13, 9);
this.darkLabel1.Name = "darkLabel1";
this.darkLabel1.Size = new System.Drawing.Size(591, 25);
this.darkLabel1.TabIndex = 21;
this.darkLabel1.Text = "Welcome and thank you for using MSFS Pop Out Panel Manager!";
//
// darkLabel2
//
this.darkLabel2.AutoSize = true;
this.darkLabel2.Font = new System.Drawing.Font("Segoe UI", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.darkLabel2.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(220)))), ((int)(((byte)(220)))), ((int)(((byte)(220)))));
this.darkLabel2.Location = new System.Drawing.Point(13, 36);
this.darkLabel2.Name = "darkLabel2";
this.darkLabel2.Size = new System.Drawing.Size(334, 20);
this.darkLabel2.TabIndex = 22;
this.darkLabel2.Text = "Instruction on how to use this utility can be found";
//
// checkBoxMinimizeToTray
//
this.checkBoxMinimizeToTray.AutoSize = true;
this.checkBoxMinimizeToTray.Font = new System.Drawing.Font("Segoe UI", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.checkBoxMinimizeToTray.Location = new System.Drawing.Point(13, 552);
this.checkBoxMinimizeToTray.Name = "checkBoxMinimizeToTray";
this.checkBoxMinimizeToTray.Size = new System.Drawing.Size(189, 24);
this.checkBoxMinimizeToTray.TabIndex = 23;
this.checkBoxMinimizeToTray.Text = "Minimize to System Tray";
//
// lblVersion
//
this.lblVersion.AutoSize = true;
this.lblVersion.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(220)))), ((int)(((byte)(220)))), ((int)(((byte)(220)))));
this.lblVersion.Location = new System.Drawing.Point(383, 561);
this.lblVersion.Name = "lblVersion";
this.lblVersion.Size = new System.Drawing.Size(48, 15);
this.lblVersion.TabIndex = 24;
this.lblVersion.Text = "Version ";
//
// StartupForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(859, 583);
this.Controls.Add(this.lblVersion);
this.Controls.Add(this.checkBoxMinimizeToTray);
this.Controls.Add(this.darkLabel2);
this.Controls.Add(this.darkLabel1);
this.Controls.Add(this.linkLabel1);
this.Controls.Add(this.labelMsfsRunning);
this.Controls.Add(this.panelStatus);
this.Controls.Add(this.panelSteps);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.Name = "StartupForm";
this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide;
this.Text = "MSFS Pop Out Panel Manager";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.StartupForm_FormClosing);
this.Load += new System.EventHandler(this.StartupForm_Load);
this.Resize += new System.EventHandler(this.StartupForm_Resize);
this.panelStatus.ResumeLayout(false);
this.panelStatus.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Panel panelSteps;
private System.Windows.Forms.Label labelMsfsRunning;
private System.Windows.Forms.Panel panelStatus;
private System.Windows.Forms.NotifyIcon notifyIcon1;
private System.Windows.Forms.LinkLabel linkLabel1;
private DarkUI.Controls.DarkLabel darkLabel1;
private DarkUI.Controls.DarkLabel darkLabel2;
private DarkUI.Controls.DarkTextBox txtBoxStatus;
private DarkUI.Controls.DarkCheckBox checkBoxMinimizeToTray;
private DarkUI.Controls.DarkLabel lblVersion;
private DarkUI.Controls.DarkLabel darkLabel3;
}
}

107
UI/StartupForm.cs Normal file
View file

@ -0,0 +1,107 @@
using DarkUI.Forms;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
namespace MSFSPopoutPanelManager
{
public partial class StartupForm : DarkForm
{
private SynchronizationContext _syncRoot;
private PanelManager _panelManager;
private UserControlPanelSelection _ucPanelSelection;
private UserControlApplySettings _ucApplySettings;
public StartupForm()
{
InitializeComponent();
_syncRoot = SynchronizationContext.Current;
// Set version number
lblVersion.Text += System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString();
Logger.OnStatusLogged += Logger_OnStatusLogged;
_panelManager = new PanelManager(this);
_panelManager.OnSimulatorStarted += PanelManager_OnSimulatorStarted;
_ucPanelSelection = new UserControlPanelSelection(_panelManager);
_ucPanelSelection.Visible = true;
_ucApplySettings = new UserControlApplySettings(_panelManager);
_ucApplySettings.OnRestart += (source, e) => { _ucPanelSelection.Visible = true; _ucApplySettings.Visible = false; };
_ucApplySettings.Visible = false;
panelSteps.Controls.Add(_ucPanelSelection);
panelSteps.Controls.Add(_ucApplySettings);
_panelManager.OnAnalysisCompleted += (source, e) => { _ucPanelSelection.Visible = false; _ucApplySettings.Visible = true; };
_panelManager.CheckSimulatorStarted();
}
private void Logger_OnStatusLogged(object sender, EventArgs<StatusMessage> e)
{
_syncRoot.Post((arg) =>
{
var msg = arg as string;
if (msg != null)
txtBoxStatus.Text = msg;
}, e.Value.Message);
}
private void PanelManager_OnSimulatorStarted(object sender, EventArgs e)
{
_syncRoot.Post((arg) =>
{
panelStatus.Enabled = true;
labelMsfsRunning.Text = "MSFS is running";
labelMsfsRunning.ForeColor = Color.LightGreen;
}, null);
}
private void StartupForm_Load(object sender, EventArgs e)
{
notifyIcon1.BalloonTipText = "Application Minimized";
notifyIcon1.BalloonTipTitle = "MSFS 2020 Pop Out Panel Manager";
}
private void StartupForm_Resize(object sender, EventArgs e)
{
if (WindowState == FormWindowState.Minimized)
{
if (checkBoxMinimizeToTray.Checked)
{
ShowInTaskbar = false;
notifyIcon1.Visible = true;
notifyIcon1.ShowBalloonTip(1000);
}
}
}
private void StartupForm_FormClosing(object sender, FormClosingEventArgs e)
{
// Put all panels popout back to original state
//_windowManager.RestorePanelTitleBar();
}
private void notifyIcon1_DoubleClick(object sender, EventArgs e)
{
ShowInTaskbar = true;
notifyIcon1.Visible = false;
WindowState = FormWindowState.Normal;
}
private void linkLabel1_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
linkLabel1.LinkVisited = true;
Process.Start(new ProcessStartInfo("https://github.com/hawkeye-stan/msfs-popout-panel-manager") { UseShellExecute = true });
}
}
}

View file

@ -1193,6 +1193,9 @@
//////////////////////////////////8= //////////////////////////////////8=
</value> </value>
</data> </data>
<metadata name="$this.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> <data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value> <value>
AAABAAEAgIAAAAEAIAAoCAEAFgAAACgAAACAAAAAAAEAAAEAIAAAAAAAAAABAHudAAB7nQAAAAAAAAAA AAABAAEAgIAAAAEAIAAoCAEAFgAAACgAAACAAAAAAAEAAAEAIAAAAAAAAAABAHudAAB7nQAAAAAAAAAA

276
UI/UserControlApplySettings.Designer.cs generated Normal file
View file

@ -0,0 +1,276 @@

namespace MSFSPopoutPanelManager
{
partial class UserControlApplySettings
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle3 = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle4 = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle5 = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle();
this.panel1 = new System.Windows.Forms.Panel();
this.dataGridViewPanels = new System.Windows.Forms.DataGridView();
this.PanelName = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Left = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Top = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Width = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Height = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.label2 = new System.Windows.Forms.Label();
this.buttonRestart = new System.Windows.Forms.Button();
this.checkBoxAlwaysOnTop = new System.Windows.Forms.CheckBox();
this.checkBoxHidePanelTitleBar = new System.Windows.Forms.CheckBox();
this.buttonApplySettings = new System.Windows.Forms.Button();
this.buttonSaveSettings = new System.Windows.Forms.Button();
this.panel1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.dataGridViewPanels)).BeginInit();
this.SuspendLayout();
//
// panel1
//
this.panel1.Controls.Add(this.dataGridViewPanels);
this.panel1.Controls.Add(this.label2);
this.panel1.ForeColor = System.Drawing.Color.White;
this.panel1.Location = new System.Drawing.Point(0, 0);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(860, 277);
this.panel1.TabIndex = 1;
//
// dataGridViewPanels
//
this.dataGridViewPanels.AllowUserToAddRows = false;
this.dataGridViewPanels.AllowUserToDeleteRows = false;
this.dataGridViewPanels.AllowUserToResizeColumns = false;
this.dataGridViewPanels.AllowUserToResizeRows = false;
this.dataGridViewPanels.CausesValidation = false;
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Control;
dataGridViewCellStyle1.Font = new System.Drawing.Font("Segoe UI", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.WindowText;
dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight;
dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
this.dataGridViewPanels.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle1;
this.dataGridViewPanels.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dataGridViewPanels.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.PanelName,
this.Left,
this.Top,
this.Width,
this.Height});
dataGridViewCellStyle3.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle3.BackColor = System.Drawing.SystemColors.Window;
dataGridViewCellStyle3.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
dataGridViewCellStyle3.ForeColor = System.Drawing.Color.White;
dataGridViewCellStyle3.SelectionBackColor = System.Drawing.SystemColors.Highlight;
dataGridViewCellStyle3.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
dataGridViewCellStyle3.WrapMode = System.Windows.Forms.DataGridViewTriState.False;
this.dataGridViewPanels.DefaultCellStyle = dataGridViewCellStyle3;
this.dataGridViewPanels.Location = new System.Drawing.Point(20, 35);
this.dataGridViewPanels.MultiSelect = false;
this.dataGridViewPanels.Name = "dataGridViewPanels";
dataGridViewCellStyle4.BackColor = System.Drawing.SystemColors.Control;
dataGridViewCellStyle4.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
dataGridViewCellStyle4.ForeColor = System.Drawing.SystemColors.WindowText;
dataGridViewCellStyle4.SelectionBackColor = System.Drawing.SystemColors.Highlight;
dataGridViewCellStyle4.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
dataGridViewCellStyle4.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
this.dataGridViewPanels.RowHeadersDefaultCellStyle = dataGridViewCellStyle4;
this.dataGridViewPanels.RowHeadersVisible = false;
dataGridViewCellStyle5.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle5.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
dataGridViewCellStyle5.ForeColor = System.Drawing.SystemColors.WindowText;
dataGridViewCellStyle5.Padding = new System.Windows.Forms.Padding(3);
this.dataGridViewPanels.RowsDefaultCellStyle = dataGridViewCellStyle5;
this.dataGridViewPanels.RowTemplate.Height = 25;
this.dataGridViewPanels.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.dataGridViewPanels.ShowCellErrors = false;
this.dataGridViewPanels.ShowCellToolTips = false;
this.dataGridViewPanels.ShowEditingIcon = false;
this.dataGridViewPanels.ShowRowErrors = false;
this.dataGridViewPanels.Size = new System.Drawing.Size(820, 225);
this.dataGridViewPanels.TabIndex = 8;
this.dataGridViewPanels.CellEndEdit += new System.Windows.Forms.DataGridViewCellEventHandler(this.dataGridViewPanels_CellEndEdit);
this.dataGridViewPanels.CellValidating += new System.Windows.Forms.DataGridViewCellValidatingEventHandler(this.dataGridViewPanels_CellValidating);
//
// PanelName
//
this.PanelName.DataPropertyName = "PanelName";
dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
this.PanelName.DefaultCellStyle = dataGridViewCellStyle2;
this.PanelName.HeaderText = "Panel Name";
this.PanelName.Name = "PanelName";
this.PanelName.ReadOnly = true;
this.PanelName.Width = 355;
//
// Left
//
this.Left.DataPropertyName = "Left";
this.Left.HeaderText = "X Pos";
this.Left.MaxInputLength = 6;
this.Left.Name = "Left";
this.Left.Width = 115;
//
// Top
//
this.Top.DataPropertyName = "Top";
this.Top.HeaderText = "Y Pos";
this.Top.MaxInputLength = 6;
this.Top.Name = "Top";
this.Top.Width = 115;
//
// Width
//
this.Width.DataPropertyName = "Width";
this.Width.HeaderText = "Width";
this.Width.MaxInputLength = 6;
this.Width.Name = "Width";
this.Width.Width = 115;
//
// Height
//
this.Height.DataPropertyName = "Height";
this.Height.HeaderText = "Height";
this.Height.MaxInputLength = 6;
this.Height.Name = "Height";
this.Height.Width = 115;
//
// label2
//
this.label2.AutoSize = true;
this.label2.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.label2.ForeColor = System.Drawing.Color.White;
this.label2.Location = new System.Drawing.Point(19, 9);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(192, 20);
this.label2.TabIndex = 7;
this.label2.Text = "Panel locations and settings";
//
// buttonRestart
//
this.buttonRestart.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(17)))), ((int)(((byte)(158)))), ((int)(((byte)(218)))));
this.buttonRestart.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.buttonRestart.ForeColor = System.Drawing.Color.White;
this.buttonRestart.Location = new System.Drawing.Point(732, 354);
this.buttonRestart.Name = "buttonRestart";
this.buttonRestart.Size = new System.Drawing.Size(107, 35);
this.buttonRestart.TabIndex = 19;
this.buttonRestart.Text = "Restart";
this.buttonRestart.UseVisualStyleBackColor = false;
this.buttonRestart.Click += new System.EventHandler(this.buttonRestart_Click);
//
// checkBoxAlwaysOnTop
//
this.checkBoxAlwaysOnTop.AutoSize = true;
this.checkBoxAlwaysOnTop.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.checkBoxAlwaysOnTop.ForeColor = System.Drawing.Color.White;
this.checkBoxAlwaysOnTop.Location = new System.Drawing.Point(20, 283);
this.checkBoxAlwaysOnTop.Name = "checkBoxAlwaysOnTop";
this.checkBoxAlwaysOnTop.Size = new System.Drawing.Size(159, 24);
this.checkBoxAlwaysOnTop.TabIndex = 20;
this.checkBoxAlwaysOnTop.Text = "Panel always on top";
this.checkBoxAlwaysOnTop.UseVisualStyleBackColor = true;
this.checkBoxAlwaysOnTop.CheckedChanged += new System.EventHandler(this.checkBoxAlwaysOnTop_CheckedChanged);
//
// checkBoxHidePanelTitleBar
//
this.checkBoxHidePanelTitleBar.AutoSize = true;
this.checkBoxHidePanelTitleBar.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.checkBoxHidePanelTitleBar.ForeColor = System.Drawing.Color.White;
this.checkBoxHidePanelTitleBar.Location = new System.Drawing.Point(207, 283);
this.checkBoxHidePanelTitleBar.Name = "checkBoxHidePanelTitleBar";
this.checkBoxHidePanelTitleBar.Size = new System.Drawing.Size(294, 24);
this.checkBoxHidePanelTitleBar.TabIndex = 21;
this.checkBoxHidePanelTitleBar.Text = "Hide panel title bar (Custom panel only)";
this.checkBoxHidePanelTitleBar.UseVisualStyleBackColor = true;
this.checkBoxHidePanelTitleBar.CheckedChanged += new System.EventHandler(this.checkBoxHidePanelTitleBar_CheckedChanged);
//
// buttonApplySettings
//
this.buttonApplySettings.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(17)))), ((int)(((byte)(158)))), ((int)(((byte)(218)))));
this.buttonApplySettings.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.buttonApplySettings.ForeColor = System.Drawing.Color.White;
this.buttonApplySettings.Location = new System.Drawing.Point(19, 354);
this.buttonApplySettings.Name = "buttonApplySettings";
this.buttonApplySettings.Size = new System.Drawing.Size(145, 35);
this.buttonApplySettings.TabIndex = 22;
this.buttonApplySettings.Text = "Apply Settings";
this.buttonApplySettings.UseVisualStyleBackColor = false;
this.buttonApplySettings.Click += new System.EventHandler(this.buttonApplySettings_Click);
//
// buttonSaveSettings
//
this.buttonSaveSettings.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(17)))), ((int)(((byte)(158)))), ((int)(((byte)(218)))));
this.buttonSaveSettings.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.buttonSaveSettings.ForeColor = System.Drawing.Color.White;
this.buttonSaveSettings.Location = new System.Drawing.Point(207, 354);
this.buttonSaveSettings.Name = "buttonSaveSettings";
this.buttonSaveSettings.Size = new System.Drawing.Size(145, 35);
this.buttonSaveSettings.TabIndex = 23;
this.buttonSaveSettings.Text = "Save Settings";
this.buttonSaveSettings.UseVisualStyleBackColor = false;
this.buttonSaveSettings.Click += new System.EventHandler(this.buttonSaveSettings_Click);
//
// UserControlApplySettings
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.Transparent;
this.Controls.Add(this.buttonSaveSettings);
this.Controls.Add(this.buttonApplySettings);
this.Controls.Add(this.checkBoxHidePanelTitleBar);
this.Controls.Add(this.checkBoxAlwaysOnTop);
this.Controls.Add(this.buttonRestart);
this.Controls.Add(this.panel1);
this.Name = "UserControlApplySettings";
this.Size = new System.Drawing.Size(860, 405);
this.panel1.ResumeLayout(false);
this.panel1.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.dataGridViewPanels)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Button buttonRestart;
private System.Windows.Forms.DataGridView dataGridViewPanels;
private System.Windows.Forms.CheckBox checkBoxAlwaysOnTop;
private System.Windows.Forms.CheckBox checkBoxHidePanelTitleBar;
private System.Windows.Forms.Button buttonApplySettings;
private System.Windows.Forms.Button buttonSaveSettings;
private System.Windows.Forms.DataGridViewTextBoxColumn PanelName;
private System.Windows.Forms.DataGridViewTextBoxColumn Left;
private System.Windows.Forms.DataGridViewTextBoxColumn Top;
private System.Windows.Forms.DataGridViewTextBoxColumn Width;
private System.Windows.Forms.DataGridViewTextBoxColumn Height;
}
}

View file

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace MSFSPopoutPanelManager
{
public partial class UserControlApplySettings : UserControlCommon
{
private BindingList<PanelDestinationInfo> _panelInfoList;
public UserControlApplySettings(PanelManager panelManager) : base(panelManager)
{
InitializeComponent();
panelManager.OnAnalysisCompleted += PanelManager_OnAnalysisCompleted;
panelManager.OnPanelSettingsChanged += PanelManager_OnAnalysisCompleted;
}
public event EventHandler OnRestart;
private void PanelManager_OnAnalysisCompleted(object sender, EventArgs e)
{
_panelInfoList = new BindingList<PanelDestinationInfo>(PanelManager.CurrentPanelProfile.PanelSettings.PanelDestinationList.OrderBy(x => x.PanelName).ToList());
dataGridViewPanels.AutoGenerateColumns = false;
dataGridViewPanels.AutoSize = false;
dataGridViewPanels.DataSource = _panelInfoList;
checkBoxAlwaysOnTop.Checked = PanelManager.CurrentPanelProfile.PanelSettings.AlwaysOnTop;
checkBoxHidePanelTitleBar.Checked = PanelManager.CurrentPanelProfile.PanelSettings.HidePanelTitleBar;
}
private void buttonRestart_Click(object sender, EventArgs e)
{
OnRestart?.Invoke(this, null);
PanelManager.UpdatePanelLocationUI();
}
private void buttonApplySettings_Click(object sender, EventArgs e)
{
PanelManager.ApplyPanelSettings();
}
private void buttonSaveSettings_Click(object sender, EventArgs e)
{
PanelManager.SavePanelSettings();
}
private void checkBoxHidePanelTitleBar_CheckedChanged(object sender, EventArgs e)
{
PanelManager.CurrentPanelProfile.PanelSettings.HidePanelTitleBar = checkBoxHidePanelTitleBar.Checked;
}
private void checkBoxAlwaysOnTop_CheckedChanged(object sender, EventArgs e)
{
PanelManager.CurrentPanelProfile.PanelSettings.AlwaysOnTop = checkBoxAlwaysOnTop.Checked;
}
private void dataGridViewPanels_CellEndEdit(object sender, DataGridViewCellEventArgs e)
{
var panelName = Convert.ToString(dataGridViewPanels[0, e.RowIndex].FormattedValue);
var left = Convert.ToInt32(dataGridViewPanels[1, e.RowIndex].FormattedValue);
var top = Convert.ToInt32(dataGridViewPanels[2, e.RowIndex].FormattedValue);
var width = Convert.ToInt32(dataGridViewPanels[3, e.RowIndex].FormattedValue);
var height = Convert.ToInt32(dataGridViewPanels[4, e.RowIndex].FormattedValue);
var panel = PanelManager.CurrentPanelProfile.PanelSettings.PanelDestinationList.Find(x => x.PanelName == panelName);
PInvoke.MoveWindow(panel.PanelHandle, left, top, width, height, true);
}
private void dataGridViewPanels_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
{
// must be numbers
if(e.ColumnIndex >= 1 && e.ColumnIndex <= 4)
{
int i = 0;
bool result = int.TryParse(Convert.ToString(e.FormattedValue), out i);
if (!result)
e.Cancel = true;
}
}
}
}

View file

@ -0,0 +1,75 @@
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="PanelName.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="Left.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="Top.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="Width.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="Height.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
</root>

21
UI/UserControlCommon.cs Normal file
View file

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace MSFSPopoutPanelManager
{
public class UserControlCommon : UserControl
{
protected PanelManager PanelManager { get; set; }
public UserControlCommon() { }
public UserControlCommon(PanelManager panelManager)
{
PanelManager = panelManager;
}
}
}

120
UI/UserControlCommon.resx Normal file
View file

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

298
UI/UserControlPanelSelection.Designer.cs generated Normal file
View file

@ -0,0 +1,298 @@

namespace MSFSPopoutPanelManager
{
partial class UserControlPanelSelection
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.panel1 = new System.Windows.Forms.Panel();
this.buttonSetDefault = new System.Windows.Forms.Button();
this.label2 = new System.Windows.Forms.Label();
this.comboBoxProfile = new System.Windows.Forms.ComboBox();
this.panel2 = new System.Windows.Forms.Panel();
this.label1 = new System.Windows.Forms.Label();
this.label5 = new System.Windows.Forms.Label();
this.label4 = new System.Windows.Forms.Label();
this.buttonPanelSelection = new System.Windows.Forms.Button();
this.label3 = new System.Windows.Forms.Label();
this.label6 = new System.Windows.Forms.Label();
this.textBoxPanelLocations = new System.Windows.Forms.TextBox();
this.panel4 = new System.Windows.Forms.Panel();
this.checkBoxShowPanelLocation = new System.Windows.Forms.CheckBox();
this.buttonAnalyze = new System.Windows.Forms.Button();
this.panel3 = new System.Windows.Forms.Panel();
this.label7 = new System.Windows.Forms.Label();
this.panel1.SuspendLayout();
this.panel2.SuspendLayout();
this.panel4.SuspendLayout();
this.panel3.SuspendLayout();
this.SuspendLayout();
//
// panel1
//
this.panel1.Controls.Add(this.buttonSetDefault);
this.panel1.Controls.Add(this.label2);
this.panel1.Controls.Add(this.comboBoxProfile);
this.panel1.ForeColor = System.Drawing.Color.White;
this.panel1.Location = new System.Drawing.Point(0, 0);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(516, 79);
this.panel1.TabIndex = 0;
//
// buttonSetDefault
//
this.buttonSetDefault.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(17)))), ((int)(((byte)(158)))), ((int)(((byte)(218)))));
this.buttonSetDefault.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.buttonSetDefault.ForeColor = System.Drawing.Color.White;
this.buttonSetDefault.Location = new System.Drawing.Point(406, 37);
this.buttonSetDefault.Name = "buttonSetDefault";
this.buttonSetDefault.Size = new System.Drawing.Size(107, 35);
this.buttonSetDefault.TabIndex = 19;
this.buttonSetDefault.Text = "Set Default";
this.buttonSetDefault.UseVisualStyleBackColor = false;
this.buttonSetDefault.Click += new System.EventHandler(this.buttonSetDefault_Click);
//
// label2
//
this.label2.AutoSize = true;
this.label2.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.label2.ForeColor = System.Drawing.Color.White;
this.label2.Location = new System.Drawing.Point(20, 10);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(315, 20);
this.label2.TabIndex = 7;
this.label2.Text = "1. Please select a profile you would like to use.";
//
// comboBoxProfile
//
this.comboBoxProfile.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.comboBoxProfile.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.comboBoxProfile.ForeColor = System.Drawing.Color.Black;
this.comboBoxProfile.FormattingEnabled = true;
this.comboBoxProfile.Location = new System.Drawing.Point(35, 41);
this.comboBoxProfile.Name = "comboBoxProfile";
this.comboBoxProfile.Size = new System.Drawing.Size(365, 28);
this.comboBoxProfile.TabIndex = 5;
this.comboBoxProfile.SelectedIndexChanged += new System.EventHandler(this.comboBoxProfile_SelectedIndexChanged);
//
// panel2
//
this.panel2.Controls.Add(this.label1);
this.panel2.Controls.Add(this.label5);
this.panel2.Controls.Add(this.label4);
this.panel2.Controls.Add(this.buttonPanelSelection);
this.panel2.Controls.Add(this.label3);
this.panel2.Location = new System.Drawing.Point(0, 80);
this.panel2.Name = "panel2";
this.panel2.Size = new System.Drawing.Size(516, 227);
this.panel2.TabIndex = 8;
//
// label1
//
this.label1.AutoSize = true;
this.label1.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.label1.ForeColor = System.Drawing.Color.White;
this.label1.Location = new System.Drawing.Point(35, 86);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(213, 20);
this.label1.TabIndex = 12;
this.label1.Text = "LEFT CLICK to add a new panel";
//
// label5
//
this.label5.AutoSize = true;
this.label5.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.label5.ForeColor = System.Drawing.Color.White;
this.label5.Location = new System.Drawing.Point(35, 146);
this.label5.Name = "label5";
this.label5.Size = new System.Drawing.Size(372, 20);
this.label5.TabIndex = 11;
this.label5.Text = "CTRL + LEFT CLICK when all panels have been selected.";
//
// label4
//
this.label4.AutoSize = true;
this.label4.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.label4.ForeColor = System.Drawing.Color.White;
this.label4.Location = new System.Drawing.Point(35, 115);
this.label4.Name = "label4";
this.label4.Size = new System.Drawing.Size(418, 20);
this.label4.TabIndex = 10;
this.label4.Text = "SHIFT + LEFT CLICK to remove the most recently added panel.";
//
// buttonPanelSelection
//
this.buttonPanelSelection.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(17)))), ((int)(((byte)(158)))), ((int)(((byte)(218)))));
this.buttonPanelSelection.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.buttonPanelSelection.ForeColor = System.Drawing.Color.White;
this.buttonPanelSelection.Location = new System.Drawing.Point(35, 177);
this.buttonPanelSelection.Name = "buttonPanelSelection";
this.buttonPanelSelection.Size = new System.Drawing.Size(170, 35);
this.buttonPanelSelection.TabIndex = 9;
this.buttonPanelSelection.Text = "Start Panel Selection";
this.buttonPanelSelection.UseVisualStyleBackColor = false;
this.buttonPanelSelection.Click += new System.EventHandler(this.buttonPanelSelection_Click);
//
// label3
//
this.label3.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.label3.ForeColor = System.Drawing.Color.White;
this.label3.Location = new System.Drawing.Point(20, 10);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(488, 63);
this.label3.TabIndex = 7;
this.label3.Text = "2. Identify the pop out panels in the game by clicking on them. Their locations w" +
"ill be saved and for use on future flights. (You only need to do this once per p" +
"rofile)";
//
// label6
//
this.label6.AutoSize = true;
this.label6.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.label6.ForeColor = System.Drawing.Color.White;
this.label6.Location = new System.Drawing.Point(103, 11);
this.label6.Name = "label6";
this.label6.Size = new System.Drawing.Size(111, 20);
this.label6.TabIndex = 11;
this.label6.Text = "Panel Locations";
//
// textBoxPanelLocations
//
this.textBoxPanelLocations.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.textBoxPanelLocations.ForeColor = System.Drawing.SystemColors.WindowText;
this.textBoxPanelLocations.Location = new System.Drawing.Point(21, 41);
this.textBoxPanelLocations.Multiline = true;
this.textBoxPanelLocations.Name = "textBoxPanelLocations";
this.textBoxPanelLocations.ReadOnly = true;
this.textBoxPanelLocations.Size = new System.Drawing.Size(301, 277);
this.textBoxPanelLocations.TabIndex = 12;
//
// panel4
//
this.panel4.Controls.Add(this.checkBoxShowPanelLocation);
this.panel4.Controls.Add(this.textBoxPanelLocations);
this.panel4.Controls.Add(this.label6);
this.panel4.Location = new System.Drawing.Point(522, 0);
this.panel4.Name = "panel4";
this.panel4.Size = new System.Drawing.Size(335, 403);
this.panel4.TabIndex = 13;
//
// checkBoxShowPanelLocation
//
this.checkBoxShowPanelLocation.AutoSize = true;
this.checkBoxShowPanelLocation.Checked = true;
this.checkBoxShowPanelLocation.CheckState = System.Windows.Forms.CheckState.Checked;
this.checkBoxShowPanelLocation.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.checkBoxShowPanelLocation.ForeColor = System.Drawing.Color.White;
this.checkBoxShowPanelLocation.Location = new System.Drawing.Point(61, 337);
this.checkBoxShowPanelLocation.Name = "checkBoxShowPanelLocation";
this.checkBoxShowPanelLocation.Size = new System.Drawing.Size(213, 24);
this.checkBoxShowPanelLocation.TabIndex = 17;
this.checkBoxShowPanelLocation.Text = "Show Panel Location Ovelay";
this.checkBoxShowPanelLocation.UseVisualStyleBackColor = true;
this.checkBoxShowPanelLocation.CheckedChanged += new System.EventHandler(this.checkBoxShowPanelLocation_CheckedChanged);
//
// buttonAnalyze
//
this.buttonAnalyze.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(17)))), ((int)(((byte)(158)))), ((int)(((byte)(218)))));
this.buttonAnalyze.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.buttonAnalyze.ForeColor = System.Drawing.Color.White;
this.buttonAnalyze.Location = new System.Drawing.Point(32, 41);
this.buttonAnalyze.Name = "buttonAnalyze";
this.buttonAnalyze.Size = new System.Drawing.Size(107, 35);
this.buttonAnalyze.TabIndex = 18;
this.buttonAnalyze.Text = "Analyze";
this.buttonAnalyze.UseVisualStyleBackColor = false;
this.buttonAnalyze.Click += new System.EventHandler(this.buttonAnalyze_Click);
//
// panel3
//
this.panel3.Controls.Add(this.buttonAnalyze);
this.panel3.Controls.Add(this.label7);
this.panel3.Enabled = false;
this.panel3.ForeColor = System.Drawing.Color.White;
this.panel3.Location = new System.Drawing.Point(3, 309);
this.panel3.Name = "panel3";
this.panel3.Size = new System.Drawing.Size(513, 94);
this.panel3.TabIndex = 14;
//
// label7
//
this.label7.AutoSize = true;
this.label7.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.label7.ForeColor = System.Drawing.Color.White;
this.label7.Location = new System.Drawing.Point(20, 10);
this.label7.Name = "label7";
this.label7.Size = new System.Drawing.Size(292, 20);
this.label7.TabIndex = 7;
this.label7.Text = "3. Pop out and analyze the selected panels.";
//
// UserControlPanelSelection
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.Transparent;
this.Controls.Add(this.panel3);
this.Controls.Add(this.panel4);
this.Controls.Add(this.panel2);
this.Controls.Add(this.panel1);
this.Name = "UserControlPanelSelection";
this.Size = new System.Drawing.Size(860, 405);
this.panel1.ResumeLayout(false);
this.panel1.PerformLayout();
this.panel2.ResumeLayout(false);
this.panel2.PerformLayout();
this.panel4.ResumeLayout(false);
this.panel4.PerformLayout();
this.panel3.ResumeLayout(false);
this.panel3.PerformLayout();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.ComboBox comboBoxProfile;
private System.Windows.Forms.Panel panel2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.Button buttonPanelSelection;
private System.Windows.Forms.Label label5;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label6;
private System.Windows.Forms.TextBox textBoxPanelLocations;
private System.Windows.Forms.Panel panel3;
private System.Windows.Forms.Label label10;
private System.Windows.Forms.Panel panel4;
private System.Windows.Forms.CheckBox checkBoxShowPanelLocation;
private System.Windows.Forms.Button buttonAnalyze;
private System.Windows.Forms.Label label7;
private System.Windows.Forms.Button buttonSetDefault;
}
}

View file

@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace MSFSPopoutPanelManager
{
public partial class UserControlPanelSelection : UserControlCommon
{
private SynchronizationContext _syncRoot;
public UserControlPanelSelection(PanelManager panelManager) : base(panelManager)
{
InitializeComponent();
_syncRoot = SynchronizationContext.Current;
panelManager.OnSimulatorStarted += (source, e) => { _syncRoot.Post((arg) => { panel3.Enabled = true; }, null); };
panelManager.PanelLocationSelection.OnSelectionStarted += PanelLocationSelection_OnSelectionStarted;
panelManager.PanelLocationSelection.OnSelectionCompleted += PanelLocationSelection_OnSelectionCompleted;
panelManager.PanelLocationSelection.OnLocationListChanged += PanelLocationSelection_OnLocationListChanged;
SetProfileDropDown();
}
public event EventHandler<EventArgs<bool>> OnAnalyzeAvailabilityChanged;
private void PanelLocationSelection_OnSelectionStarted(object sender, EventArgs e)
{
buttonPanelSelection.Enabled = false;
buttonAnalyze.Enabled = false;
OnAnalyzeAvailabilityChanged?.Invoke(this, new EventArgs<bool>(false));
}
private void PanelLocationSelection_OnSelectionCompleted(object sender, EventArgs e)
{
buttonPanelSelection.Enabled = true;
buttonAnalyze.Enabled = PanelManager.CurrentPanelProfile.PanelSourceCoordinates.Count > 0;
OnAnalyzeAvailabilityChanged?.Invoke(this, new EventArgs<bool>(true));
buttonAnalyze.Focus();
}
private void PanelLocationSelection_OnLocationListChanged(object sender, EventArgs e)
{
var sb = new StringBuilder();
if (PanelManager.CurrentPanelProfile.PanelSourceCoordinates.Count == 0)
{
textBoxPanelLocations.Text = null;
}
else
{
foreach (var coor in PanelManager.CurrentPanelProfile.PanelSourceCoordinates)
{
sb.Append($"Panel: {coor.PanelIndex,-7} X-Pos: {coor.X,-10} Y-Pos: {coor.Y,-10}");
sb.Append(Environment.NewLine);
}
textBoxPanelLocations.Text = sb.ToString();
}
}
public void SetProfileDropDown()
{
try
{
var defaultProfileId = FileManager.ReadUserData().DefaultProfileId;
var profileData = FileManager.ReadPlaneProfileData();
comboBoxProfile.DisplayMember = "ProfileName";
comboBoxProfile.ValueMember = "ProfileId";
comboBoxProfile.DataSource = profileData.OrderBy(x => x.ProfileName).ToList();
comboBoxProfile.SelectedValue = defaultProfileId;
}
catch (Exception ex)
{
Logger.LogStatus(ex.Message);
}
}
private void buttonPanelSelection_Click(object sender, EventArgs e)
{
bool continued = true;
if (PanelManager.CurrentPanelProfile != null && PanelManager.CurrentPanelProfile.PanelSettings.PanelDestinationList.Count > 0)
{
var dialogResult = MessageBox.Show("Are you sure you want to overwrite existing saved panel locations and settings for this profile?", "Confirm Overwrite", MessageBoxButtons.YesNo);
continued = dialogResult == DialogResult.Yes;
}
if (continued)
{
PanelManager.PanelLocationSelection.Start();
checkBoxShowPanelLocation.Checked = true;
}
}
private void comboBoxProfile_SelectedIndexChanged(object sender, EventArgs e)
{
PanelManager.PlaneProfileChanged(Convert.ToInt32(comboBoxProfile.SelectedValue), checkBoxShowPanelLocation.Checked);
buttonPanelSelection.Enabled = true;
}
private void checkBoxShowPanelLocation_CheckedChanged(object sender, EventArgs e)
{
PanelManager.PanelLocationSelection.ShowPanelLocationOverlay(checkBoxShowPanelLocation.Checked);
}
private void buttonAnalyze_Click(object sender, EventArgs e)
{
Logger.LogStatus("Panel analysis in progress. Please wait...");
panel1.Enabled = false;
panel2.Enabled = false;
panel3.Enabled = false;
PanelManager.Analyze();
panel1.Enabled = true;
panel2.Enabled = true;
panel3.Enabled = true;
}
private void buttonSetDefault_Click(object sender, EventArgs e)
{
PanelManager.SetDefaultProfile();
}
}
}

View file

@ -0,0 +1,60 @@
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View file

@ -1,30 +0,0 @@
using System.Collections.Generic;
namespace MSFSPopoutPanelManager
{
public class UserData
{
public UserData()
{
Profiles = new List<Profile>();
}
public List<Profile> Profiles { get; set; }
}
public class Profile
{
public Profile()
{
PopoutNames = new Dictionary<string, Rect>();
}
public string Name { get; set; }
public bool AlwaysOnTop { get; set; }
public bool HidePanelTitleBar { get; set; }
public Dictionary<string, Rect> PopoutNames;
}
}

View file

@ -1,6 +1,13 @@
# Version History # Version History
<hr/> <hr/>
## Version 2.0.0.0
* Used new image recognition instead of OCR technology to determine pop outs.
* Added auto pop out feature.
* Allowed moving pop out panels using coordinates/width/height after analysis.
* Added additional plane profiles.
* Running on non-native monitor resolution will not work because of image scaling issue when doing image analysis.
## Version 1.2.0.0 ## Version 1.2.0.0
* Increase OCR image accuracy by raising image DPI before analysis. * Increase OCR image accuracy by raising image DPI before analysis.
* Added (very experimental) Asobo A320 and FlybyWire A320NX profiles as testing sample. These profiles do only work 100% of the time. Continue investigation into better OCR accuracy will be needed. * Added (very experimental) Asobo A320 and FlybyWire A320NX profiles as testing sample. These profiles do only work 100% of the time. Continue investigation into better OCR accuracy will be needed.
@ -9,7 +16,7 @@
* Fixed an issue of switching profiles will cause panels to be out of sync. * Fixed an issue of switching profiles will cause panels to be out of sync.
* Fixed an issue of unable to set or reset panel to NOT ALWAYS ON TOP. * Fixed an issue of unable to set or reset panel to NOT ALWAYS ON TOP.
* Fixed application path issue for not able to find ocrdata.json file at startup. * Fixed application path issue for not able to find ocrdata.json file at startup.
* Removed MSFS Pop Out Panel Manager is always on top (not the actual in-game panels themselves). Now it is only always on top during development debug mode if you compile and run the application from source code. * Removed MSFS Pop Out Panel Manager is always on top. This is intefering with image operations.
## Version 1.1.0.0 ## Version 1.1.0.0
* Added caption title for the "untitled" windows. After analysis, if the panel window matches the name in the profile/ocr definition file, it will now display a caption of "Custom - XXXXX" (ie. Custom - PFD). This allows user to use various 3rd party windows layout manager to organize pop out panel windows. * Added caption title for the "untitled" windows. After analysis, if the panel window matches the name in the profile/ocr definition file, it will now display a caption of "Custom - XXXXX" (ie. Custom - PFD). This allows user to use various 3rd party windows layout manager to organize pop out panel windows.

View file

@ -1,269 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Timers;
namespace MSFSPopoutPanelManager
{
public class WindowManager
{
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hwnd, ref Rect rectangle);
[DllImport("user32.dll")]
public static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("USER32.dll")]
public static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong);
[DllImport("user32.dll")]
public static extern IntPtr SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, int wFlags);
[DllImport("user32.dll")]
static extern bool MoveWindow(IntPtr hWnd, int x, int y, int width, int height, bool repaint);
const int SWP_NOMOVE = 0x0002;
const int SWP_NOSIZE = 0x0001;
const int SWP_ALWAYS_ON_TOP = SWP_NOMOVE | SWP_NOSIZE;
const int GWL_STYLE = -16;
const int WS_SIZEBOX = 0x00040000;
const int WS_BORDER = 0x00800000;
const int WS_DLGFRAME = 0x00400000;
const int WS_CAPTION = WS_BORDER | WS_DLGFRAME;
private const int MSFS_CONNECTION_RETRY_TIMEOUT = 2000;
private FileManager _fileManager;
private Timer _timer;
private UserData _userData;
private MainWindow _simWindow;
private AnalysisEngine _analysisEngine;
private Dictionary<IntPtr, Int64> _originalChildWindowStyles;
private bool _currentHidePanelTitleBarStatus;
private bool _currentAlwaysOnTopStatus;
public WindowManager(FileManager fileManager)
{
_fileManager = fileManager;
_analysisEngine = new AnalysisEngine();
_analysisEngine.OnStatusUpdated += (source, e) => OnStatusUpdated?.Invoke(source, e);
_analysisEngine.OnOcrDebugged += (source, e) => OnOcrDebugged?.Invoke(source, e);
}
public event EventHandler<EventArgs<string>> OnStatusUpdated;
public event EventHandler OnSimulatorStarted;
public event EventHandler<EventArgs<Dictionary<string, string>>> OnOcrDebugged;
public void CheckSimulatorStarted()
{
// Autoconnect to flight simulator
_timer = new Timer();
_timer.Interval = MSFS_CONNECTION_RETRY_TIMEOUT;
_timer.Enabled = true;
_timer.Elapsed += (source, e) =>
{
var simulatorConnected = GetSimulatorWindow();
if (simulatorConnected)
{
OnSimulatorStarted?.Invoke(this, null);
_timer.Enabled = false;
}
};
}
public void Reset()
{
// reset these statuses
_currentHidePanelTitleBarStatus = false;
_currentAlwaysOnTopStatus = false;
RestorePanelTitleBar();
_originalChildWindowStyles = null;
}
public bool Analyze(string profileName)
{
_originalChildWindowStyles = null;
_simWindow.ChildWindowsData = new List<ChildWindow>();
var evalData = _fileManager.ReadProfileData().Find(x => x.Profile == profileName);
_analysisEngine.Analyze(ref _simWindow, evalData);
return _simWindow.ChildWindowsData.FindAll(x => x.PopoutType == PopoutType.Custom || x.PopoutType == PopoutType.BuiltIn).Count > 0;
}
public void ApplySettings(string profileName, bool hidePanelTitleBar, bool alwaysOnTop)
{
// Try to load previous profiles
_userData = _fileManager.ReadUserData();
var profileSettings = _userData != null ? _userData.Profiles.Find(x => x.Name == profileName) : null;
if (profileSettings == null)
{
OnStatusUpdated?.Invoke(this, new EventArgs<string>("Profile settings does not exist. Please move pop out panels to desire location and click Save Settings."));
return;
}
// select all valid windows
var childWindows = _simWindow.ChildWindowsData.FindAll(x => x.PopoutType == PopoutType.Custom || x.PopoutType == PopoutType.BuiltIn);
if (childWindows.Count > 0)
{
ApplyPositions(profileSettings, childWindows);
if (_currentHidePanelTitleBarStatus != hidePanelTitleBar)
{
_currentHidePanelTitleBarStatus = hidePanelTitleBar;
ApplyHidePanelTitleBar(hidePanelTitleBar, childWindows);
}
if(_currentAlwaysOnTopStatus != alwaysOnTop)
{
_currentAlwaysOnTopStatus = alwaysOnTop;
ApplyAlwaysOnTop(alwaysOnTop, childWindows);
}
}
}
public void SaveSettings(string profileName, bool hidePanelTitleBar, bool alwaysOnTop)
{
if (_userData == null)
_userData = new UserData();
var profile = _userData.Profiles.Find(x => x.Name == profileName);
if (profile == null)
{
profile = new Profile() { Name = profileName, AlwaysOnTop = alwaysOnTop, HidePanelTitleBar = hidePanelTitleBar };
_userData.Profiles.Add(profile);
}
else
{
profile.HidePanelTitleBar = hidePanelTitleBar;
profile.AlwaysOnTop = alwaysOnTop;
}
if (_simWindow.ChildWindowsData.Count > 0)
{
foreach (var window in _simWindow.ChildWindowsData)
{
if (!window.Title.Contains("Failed Analysis"))
{
var rect = new Rect();
GetWindowRect(window.Handle, ref rect);
if (!profile.PopoutNames.TryAdd(window.Title, rect))
profile.PopoutNames[window.Title] = rect;
}
}
_fileManager.WriteUserData(_userData);
OnStatusUpdated?.Invoke(this, new EventArgs<string>("Pop out panel positions have been saved."));
}
}
public void RestorePanelTitleBar()
{
if (_simWindow != null)
{
var childWindows = _simWindow.ChildWindowsData.FindAll(x => x.PopoutType == PopoutType.Custom || x.PopoutType == PopoutType.BuiltIn);
ApplyHidePanelTitleBar(false, childWindows);
}
}
private void ApplyPositions(Profile userProfile, List<ChildWindow> childWindows)
{
foreach (var childWindow in childWindows)
{
var hasCoordinates = userProfile.PopoutNames.ContainsKey(childWindow.Title);
if (hasCoordinates)
{
var rect = userProfile.PopoutNames[childWindow.Title];
MoveWindow(childWindow.Handle, rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top, true);
}
}
}
private void ApplyAlwaysOnTop(bool alwaysOnTop, List<ChildWindow> childWindows)
{
if (alwaysOnTop)
{
foreach (var childWindow in childWindows)
{
Rect rect = new Rect();
GetWindowRect(childWindow.Handle, ref rect);
SetWindowPos(childWindow.Handle, new IntPtr(-1), rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top, SWP_ALWAYS_ON_TOP);
}
}
else
{
foreach (var childWindow in childWindows)
{
Rect rect = new Rect();
GetWindowRect(childWindow.Handle, ref rect);
SetWindowPos(childWindow.Handle, new IntPtr(-2), rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top, 0);
}
}
}
private void ApplyHidePanelTitleBar(bool hidePanelTitleBar, List<ChildWindow> childWindows)
{
if (hidePanelTitleBar)
{
_originalChildWindowStyles = new Dictionary<IntPtr, Int64>();
foreach (var childWindow in childWindows)
{
// Save the current panel title bar styles so we can restore it later
if (!_originalChildWindowStyles.ContainsKey(childWindow.Handle))
_originalChildWindowStyles[childWindow.Handle] = GetWindowLong(childWindow.Handle, GWL_STYLE).ToInt64();
SetWindowLong(childWindow.Handle, GWL_STYLE, (uint)(_originalChildWindowStyles[childWindow.Handle] & ~(WS_CAPTION | WS_SIZEBOX)));
}
}
else
{
if (_originalChildWindowStyles != null)
{
foreach (var childWindow in childWindows)
{
if (_originalChildWindowStyles.ContainsKey(childWindow.Handle))
{
SetWindowLong(childWindow.Handle, GWL_STYLE, (uint)_originalChildWindowStyles[childWindow.Handle]);
_originalChildWindowStyles.Remove(childWindow.Handle);
}
}
}
}
}
private bool GetSimulatorWindow()
{
// Get flight simulator process
foreach (var process in Process.GetProcesses())
{
if (process.ProcessName == "FlightSimulator" && _simWindow == null)
{
_simWindow = new MainWindow()
{
ProcessId = process.Id,
ProcessName = process.ProcessName,
Title = "Microsoft Flight Simulator",
Handle = process.MainWindowHandle
};
return true;
}
}
return false;
}
}
}

View file

@ -0,0 +1,147 @@
[
{
"templateName": "G1000",
"templates": [
{
"popoutId": 1,
"popoutName": "PFD",
"imagePaths": [ "g1000/g1000_pfd.png" ]
},
{
"popoutId": 2,
"popoutName": "MFD",
"imagePaths": [ "g1000/g1000_mfd.png", "g1000/g1000_mfd2.png" ]
}
]
},
{
"templateName": "WT-G1000NXi",
"templates": [
{
"popoutId": 1,
"popoutName": "PFD",
"imagePaths": [ "g1000nxi/g1000nxi_pfd.png" ]
},
{
"popoutId": 2,
"popoutName": "MFD",
"imagePaths": [ "g1000nxi/g1000nxi_mfd.png", "g1000nxi/g1000nxi_mfd2.png" ]
}
]
},
{
"templateName": "G3000-KINGAIR",
"templates": [
{
"popoutId": 1,
"popoutName": "PFD",
"imagePaths": [ "g3000-kingair/g3000kingair_pfd.png", "g3000-kingair/g3000kingair_pfd2.png" ]
},
{
"popoutId": 2,
"popoutName": "MFD",
"imagePaths": [ "g3000-kingair/g3000kingair_mfd.png", "g3000-kingair/g3000kingair_mfd2.png" ]
},
{
"popoutId": 3,
"popoutName": "Standby Altitude Module",
"imagePaths": [ "g3000-kingair/g3000kingair_standby_altitude_module.png" ]
}
]
},
{
"templateName": "G3000",
"templates": [
{
"popoutId": 1,
"popoutName": "PFD",
"imagePaths": [ "g3000/g3000_pfd.png" ]
},
{
"popoutId": 2,
"popoutName": "MFD",
"imagePaths": [ "g3000/g3000_mfd.png", "g3000/g3000_mfd2.png" ]
},
{
"popoutId": 3,
"popoutName": "Multipurpose Control Display Unit",
"imagePaths": [ "g3000/g3000_multipurpose_control.png" ]
},
{
"popoutId": 4,
"popoutName": "Standby Altitude Module #1",
"imagePaths": [ "g3000/g3000_standby_altitude_module_1.png" ]
},
{
"popoutId": 5,
"popoutName": "Standby Altitude Module #2",
"imagePaths": [ "g3000/g3000_standby_altitude_module_2.png" ]
}
]
},
{
"templateName": "FBW-A32NX",
"templates": [
{
"popoutId": 1,
"popoutName": "Message Panel",
"imagePaths": [ "a32nx/a32nx_message_panel.png" ]
},
{
"popoutId": 2,
"popoutName": "System Display",
"imagePaths": [ "a32nx/a32nx_system_display.png" ]
},
{
"popoutId": 3,
"popoutName": "Engine Display",
"imagePaths": [ "a32nx/a32nx_engine_display.png" ]
},
{
"popoutId": 4,
"popoutName": "Multipurpose Control Display Unit",
"imagePaths": [ "a32nx/a32nx_multipurpose_control.png" ]
},
{
"popoutId": 5,
"popoutName": "NAV Display",
"imagePaths": [ "a32nx/a32nx_nav_display.png" ]
},
{
"popoutId": 6,
"popoutName": "PFD",
"imagePaths": [ "a32nx/a32nx_pfd.png" ]
},
{
"popoutId": 7,
"popoutName": "Standby Altitude Indicator",
"imagePaths": [ "a32nx/a32nx_standby_altitude_indicator.png" ]
}
]
},
{
"templateName": "CJ4",
"templates": [
{
"popoutId": 1,
"popoutName": "PFD",
"imagePaths": [ "cj4/cj4_pfd.png" ]
},
{
"popoutId": 2,
"popoutName": "MFD",
"imagePaths": [ "cj4/cj4_mfd.png" ]
},
{
"popoutId": 3,
"popoutName": "Standby Altitude Module",
"imagePaths": [ "cj4/cj4_standby_altitude_module.png" ]
},
{
"popoutId": 4,
"popoutName": "Multipurpose Control Display Unit",
"imagePaths": [ "cj4/cj4_multipurpose_control.png" ]
}
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View file

@ -1,106 +0,0 @@
[
// G1000 NXi
{
"profile": "G1000 / G1000 NXi",
"defaultProfile": "true",
"ocrImageScale": 1.0,
"evalData": [
{
"popoutName": "PFD",
"data": [ "PFD", "XPDR", "Nearest", "MAP/HSI", "Ident", "Tmr/Ref" ]
},
{
"popoutName": "MFD",
"data": [ "FUEL", "QTY", "GAL", "FFLOW GPH", "OIL", "PRESS", "ELECTRICAL", "BATT", "VOLTS", "Navigation Map" ]
}
]
},
// G3000
{
"profile": "G3000",
"defaultProfile": "false",
"ocrImageScale": 1.0,
"evalData": [
{
"popoutName": "FMS",
"data": [ "PFD Home", "NAV SOURCE", "Bearing 1", "Bearing 2", "Bearing", "Speed Bugs", "Timers", "Minimums", "PDF Settings" ]
},
{
"popoutName": "PFD",
"data": [ "PFD", "PDF Map", "Ranges", "Range+", "Range-", "Active Nav", "WX Radar", "WX", "Controls" ]
},
{
"popoutName": "MFD",
"data": [ "STBY", "PROP", "FUEL", "OIL", "OIL PSI", "PRESS", "CABIN PRESS", "DIFF PSI", "ELECTRICAL", "BATT", "VOLTS", "Navigation Map" ]
}
]
},
// Flybywire A320NX
{
"profile": "Flybywire A320NX",
"defaultProfile": "false",
"ocrImageScale": 1,
"evalData": [
{
"popoutName": "Screen1",
"data": [ "QNH", "29.92" ]
},
{
"popoutName": "Screen2",
"data": [ "REL", "GW" ]
},
{
"popoutName": "Screen3",
"data": [ "TAS", "GS", "NM", "GPS", "Primary", "PPOS" ]
},
{
"popoutName": "Screen4",
"data": [ "F.FLOW", "AUTO BRK", "KG/MIN", "FOB", "TAT", "SAT" ]
}
]
},
// Asobo A320
{
"profile": "Asobo A320",
"defaultProfile": "false",
"ocrImageScale": 1,
"evalData": [
{
"popoutName": "Screen1",
"data": [ "IDLE" ]
},
{
"popoutName": "Screen2",
"data": [ "TAS", "GS", "NM" ]
},
{
"popoutName": "Screen3",
"data": [ "F.FLOW", "KG", "KG/MIN", "FOB", "TAT", "SAT" ]
},
{
"popoutName": "Screen4",
"data": [ "SPD", "SEL", "CLB", "NAV", "QNH" ]
}
]
},
// Custom Sample Profile - delete this if you don't need it
{
"profile": "ZZZ - Custom Sample Profile",
"defaultProfile": "false",
"ocrImageScale": 1.0,
"evalData": [
{
"popoutName": "Whatever Panel 1",
"data": [ "Text to detect 1", "Text to detect 2" ]
},
{
"popoutName": "Whatever Panel 2",
"data": [ "Text to detect 3", "Text to detect 4" ]
},
{
"popoutName": "Whatever Panel 3",
"data": [ "Text to detect 4", "Text to detect 4" ]
}
]
}
]

37
config/planeprofile.json Normal file
View file

@ -0,0 +1,37 @@
[
{
"profileId": 1,
"profileName": "Cessna 172 Skyhawk (WT G1000 NXi)",
"analysisTemplateName": "WT-G1000NXi"
},
{
"profileId": 2,
"profileName": "DA62 (WT G1000 NXi)",
"analysisTemplateName": "WT-G1000NXi"
},
{
"profileId": 3,
"profileName": "A32NX (Flybywire) ",
"analysisTemplateName": "FBW-A32NX"
},
{
"profileId": 4,
"profileName": "Beechcraft King Air 350i (Original G3000)",
"analysisTemplateName": "G3000-KINGAIR"
},
{
"profileId": 5,
"profileName": "TBM 930 (Original G3000)",
"analysisTemplateName": "G3000"
},
{
"profileId": 6,
"profileName": "Cessna Citation CJ4",
"analysisTemplateName": "CJ4"
},
{
"profileId": 7,
"profileName": "Asobo Planes with (Original G1000)",
"analysisTemplateName": "G1000"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View file

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

BIN
images/doc/screenshot2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

BIN
images/doc/screenshot3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
images/doc/screenshot4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

BIN
images/doc/screenshot5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
images/doc/screenshot6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
images/doc/screenshot7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

BIN
images/transparent.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB