1
0
Fork 0
mirror of https://github.com/hawkeye-stan/msfs-popout-panel-manager.git synced 2024-11-22 05:40:11 +00:00
msfs-popout-panel-manager/Provider/PanelPopoutManager.cs
2022-07-09 18:52:47 -04:00

561 lines
23 KiB
C#

using MSFSPopoutPanelManager.Model;
using MSFSPopoutPanelManager.Shared;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MSFSPopoutPanelManager.Provider
{
public class PanelPopOutManager
{
private const int RETRY_COUNT = 5;
private UserProfileManager _userProfileManager;
private SimConnectManager _simConnectManager;
private IntPtr _simulatorHandle;
private List<PanelConfig> _panels;
private int _currentPanelIndex;
public event EventHandler OnPopOutStarted;
public event EventHandler<EventArgs<bool>> OnPopOutCompleted;
public UserProfile UserProfile { get; set; }
public AppSetting AppSetting { get; set; }
public PanelPopOutManager(UserProfileManager userProfileManager, SimConnectManager simConnectManager)
{
_userProfileManager = userProfileManager;
_simConnectManager = simConnectManager;
}
public void StartPopout()
{
var simulatorProcess = DiagnosticManager.GetSimulatorProcess();
if (simulatorProcess != null)
_simulatorHandle = simulatorProcess.Handle;
_panels = new List<PanelConfig>();
OnPopOutStarted?.Invoke(this, null);
if (AppSetting.UseAutoPanning)
InputEmulationManager.LoadCustomView(AppSetting.AutoPanningKeyBinding);
Task<List<PanelConfig>> popoutPanelTask = Task<List<PanelConfig>>.Factory.StartNew(() =>
{
return ExecutePopoutSeparation();
});
popoutPanelTask.Wait();
var popoutResults = popoutPanelTask.Result;
if (popoutResults != null)
{
if (UserProfile.PanelConfigs.Count > 0)
{
LoadAndApplyPanelConfigs(popoutResults);
Logger.LogStatus("Panels have been popped out succesfully and saved panel settings have been applied.", StatusMessageType.Info);
}
else
{
UserProfile.PanelConfigs = new ObservableCollection<PanelConfig>(popoutResults);
Logger.LogStatus("Panels have been popped out succesfully.", StatusMessageType.Info);
}
// Recenter the view port by Ctrl-Space, needs to click on game window
var simualatorProcess = DiagnosticManager.GetSimulatorProcess();
if (simualatorProcess != null)
{
InputEmulationManager.CenterView(simualatorProcess.Handle);
}
_userProfileManager.WriteUserProfiles();
OnPopOutCompleted?.Invoke(this, new EventArgs<bool>(true));
}
else
{
OnPopOutCompleted?.Invoke(this, new EventArgs<bool>(false));
}
}
public List<PanelConfig> ExecutePopoutSeparation()
{
_currentPanelIndex = 0;
_panels.Clear();
// Must close out all existing custom pop out panels
PInvoke.EnumWindows(new PInvoke.CallBack(EnumCustomPopoutCallBack), 0);
if (_panels.Count > 0)
{
Logger.LogStatus("Please close all existing panel pop outs before continuing.", StatusMessageType.Error);
return null;
}
_panels.Clear();
if (_simulatorHandle != IntPtr.Zero)
PInvoke.SetForegroundWindow(_simulatorHandle);
try
{
// PanelIndex starts at 1
for (var i = 1; i <= UserProfile.PanelSourceCoordinates.Count; i++)
{
PopoutPanel(UserProfile.PanelSourceCoordinates[i - 1].X, UserProfile.PanelSourceCoordinates[i - 1].Y);
if (i > 1)
{
SeparatePanel(i - 1, _panels[0].PanelHandle); // The joined panel is always the first panel that got popped out
}
var handle = PInvoke.FindWindow("AceApp", String.Empty);
if (handle == IntPtr.Zero && i == 1)
throw new PopoutManagerException("Unable to pop out the first panel. Please check the first panel's number circle is positioned inside the panel, check for panel obstruction, and check if panel can be popped out. Pop out process stopped.");
else if (handle == IntPtr.Zero)
throw new PopoutManagerException($"Unable to pop out panel number {i}. Please check panel's number circle is positioned inside the panel, check for panel obstruction, and check if panel can be popped out. Pop out process stopped.");
var panelInfo = GetPanelWindowInfo(handle);
panelInfo.PanelIndex = i;
panelInfo.PanelName = $"Panel{i}";
_panels.Add(panelInfo);
PInvoke.SetWindowText(panelInfo.PanelHandle, panelInfo.PanelName + " (Custom)");
if (i > 1)
WindowManager.MoveWindow(panelInfo.PanelHandle, panelInfo.PanelType, 0, 0, 800, 600);
}
_currentPanelIndex = _panels.Count;
// Performance validation, make sure the number of pop out panels is equal to the number of selected panel
if (GetPopoutPanelCountByType(PanelType.CustomPopout) != UserProfile.PanelSourceCoordinates.Count)
throw new PopoutManagerException("Unable to pop out all panels. Please align all panel number circles with in-game panel locations.");
// Add the built-in pop outs (ie. ATC, VFR Map) to the panel list
if (AppSetting.IncludeBuiltInPanel)
PInvoke.EnumWindows(new PInvoke.CallBack(EnumBuiltinPopoutCallBack), 0);
// Add the MSFS Touch Panel (My other github project) windows to the panel list
PInvoke.EnumWindows(new PInvoke.CallBack(EnumMSFSTouchPanelPopoutCallBack), 0);
if (_panels.Count == 0)
throw new PopoutManagerException("No panels have been found. Please select at least one in-game panel.");
// Line up all the panels and fill in meta data
for (var i = _panels.Count - 1; i >= 0; i--)
{
if (_panels[i].PanelType == PanelType.CustomPopout)
{
var shift = _panels.Count - i - 1;
_panels[i].Top = shift * 30;
_panels[i].Left = shift * 30;
_panels[i].Width = 800;
_panels[i].Height = 600;
WindowManager.MoveWindow(_panels[i].PanelHandle, _panels[i].PanelType, _panels[i].Top, _panels[i].Left, _panels[i].Width, _panels[i].Height);
PInvoke.SetForegroundWindow(_panels[i].PanelHandle);
Thread.Sleep(200);
}
}
return _panels;
}
catch (PopoutManagerException ex)
{
Logger.LogStatus(ex.Message, StatusMessageType.Error);
return null;
}
catch
{
throw;
}
}
private void LoadAndApplyPanelConfigs(List<PanelConfig> popoutResults)
{
int index;
popoutResults.ForEach(resultPanel =>
{
if (resultPanel.PanelType == PanelType.CustomPopout)
{
index = UserProfile.PanelConfigs.ToList().FindIndex(x => x.PanelIndex == resultPanel.PanelIndex);
if (index > -1)
UserProfile.PanelConfigs[index].PanelHandle = resultPanel.PanelHandle;
}
else
{
index = UserProfile.PanelConfigs.ToList().FindIndex(x => x.PanelName == resultPanel.PanelName);
if (index > -1)
UserProfile.PanelConfigs[index].PanelHandle = resultPanel.PanelHandle;
else
UserProfile.PanelConfigs.Add(resultPanel);
}
});
// Remove pop out that do not exist for this pop out iteration
//foreach(var panelConfig in UserProfile.PanelConfigs.ToList())
//{
// if(panelConfig.PanelHandle == IntPtr.Zero)
// {
// UserProfile.PanelConfigs.Remove(panelConfig);
// }
//}
Parallel.ForEach(UserProfile.PanelConfigs, panel =>
{
if (panel != null && panel.PanelHandle != IntPtr.Zero && panel.Width != 0 && panel.Height != 0)
{
// Apply panel name
if (panel.PanelType == PanelType.CustomPopout)
{
var name = panel.PanelName;
if (name.IndexOf("(Custom)") == -1)
name = name + " (Custom)";
PInvoke.SetWindowText(panel.PanelHandle, name);
Thread.Sleep(500);
}
if (!panel.FullScreen)
{
// Apply locations
PInvoke.ShowWindow(panel.PanelHandle, PInvokeConstant.SW_RESTORE);
Thread.Sleep(250);
WindowManager.MoveWindow(panel.PanelHandle, panel.PanelType, panel.Left, panel.Top, panel.Width, panel.Height);
Thread.Sleep(1000);
// Built-in panels (ie. Checklist, ATC) needs another window resize since there is a bug where when move between
// monitors, it changes its size
if (panel.PanelType == PanelType.BuiltInPopout)
{
PInvoke.MoveWindow(panel.PanelHandle, panel.Left, panel.Top, panel.Width, panel.Height, false);
Thread.Sleep(1000);
}
// Apply always on top
if (panel.AlwaysOnTop)
{
WindowManager.ApplyAlwaysOnTop(panel.PanelHandle, panel.PanelType, true, new Rectangle(panel.Left, panel.Top, panel.Width, panel.Height));
Thread.Sleep(1000);
}
// Apply hide title bar
if (panel.HideTitlebar)
{
WindowManager.ApplyHidePanelTitleBar(panel.PanelHandle, true);
}
}
PInvoke.ShowWindow(panel.PanelHandle, PInvokeConstant.SW_RESTORE);
}
});
// Apply full screen (cannot combine with always on top or hide title bar)
// Cannot run in parallel process
UserProfile.PanelConfigs.ToList().ForEach(panel =>
{
if (panel.FullScreen && (!panel.AlwaysOnTop && !panel.HideTitlebar))
{
WindowManager.MoveWindow(panel.PanelHandle, panel.Left, panel.Top);
Thread.Sleep(500);
InputEmulationManager.ToggleFullScreenPanel(panel.PanelHandle);
Thread.Sleep(250);
}
});
}
private int GetPopoutPanelCountByType(PanelType panelType)
{
return _panels.FindAll(x => x.PanelType == panelType).Count;
}
private PanelConfig GetCustomPopoutPanelByIndex(int index)
{
return _panels.Find(x => x.PanelType == PanelType.CustomPopout && x.PanelIndex == index + 1);
}
private void PopoutPanel(int x, int y)
{
InputEmulationManager.PopOutPanel(x, y);
}
private void SeparatePanel(int index, IntPtr hwnd)
{
// Resize all windows to 800x600 when separating and shimmy the panel
// MSFS draws popout panel differently at different time for same panel
WindowManager.MoveWindow(hwnd, PanelType.CustomPopout, -8, 0, 800, 600);
PInvoke.SetForegroundWindow(hwnd);
Thread.Sleep(500);
InputEmulationManager.LeftClick(0, 0);
// Find the magnifying glass coordinate
var point = AnalyzeMergedWindows(hwnd);
InputEmulationManager.LeftClick(point.X, point.Y);
}
public bool EnumCustomPopoutCallBack(IntPtr hwnd, int lParam)
{
var panelInfo = GetPanelWindowInfo(hwnd);
if (panelInfo != null && panelInfo.PanelType == PanelType.CustomPopout)
{
if (!_panels.Exists(x => x.PanelHandle == hwnd))
{
Interlocked.Increment(ref _currentPanelIndex);
panelInfo.PanelIndex = _currentPanelIndex;
_panels.Add(panelInfo);
}
}
return true;
}
public bool EnumBuiltinPopoutCallBack(IntPtr hwnd, int lParam)
{
var panelInfo = GetPanelWindowInfo(hwnd);
if (panelInfo != null && panelInfo.PanelType == PanelType.BuiltInPopout)
{
if (!_panels.Exists(x => x.PanelHandle == hwnd))
{
Interlocked.Increment(ref _currentPanelIndex);
panelInfo.PanelIndex = _currentPanelIndex;
_panels.Add(panelInfo);
}
}
return true;
}
public bool EnumMSFSTouchPanelPopoutCallBack(IntPtr hwnd, int index)
{
var panelInfo = GetPanelWindowInfo(hwnd);
if (panelInfo != null && panelInfo.PanelType == PanelType.MSFSTouchPanel)
{
if (!_panels.Exists(x => x.PanelHandle == hwnd))
{
Interlocked.Increment(ref _currentPanelIndex);
panelInfo.PanelIndex = _currentPanelIndex;
_panels.Add(panelInfo);
// Apply always on top to these panels
//WindowManager.ApplyAlwaysOnTop(panelInfo.PanelHandle, true);
}
}
return true;
}
private PanelConfig GetPanelWindowInfo(IntPtr hwnd)
{
var className = PInvoke.GetClassName(hwnd);
if (className == "AceApp") // MSFS windows designation
{
var caption = PInvoke.GetWindowText(hwnd);
Rectangle rectangle;
PInvoke.GetWindowRect(hwnd, out rectangle);
var panelInfo = new PanelConfig();
panelInfo.PanelHandle = hwnd;
panelInfo.PanelName = caption;
panelInfo.Top = rectangle.Top;
panelInfo.Left = rectangle.Left;
panelInfo.Width = rectangle.Width;
panelInfo.Height = rectangle.Height;
if (String.IsNullOrEmpty(caption) || caption.IndexOf("Custom") > -1)
panelInfo.PanelType = PanelType.CustomPopout;
else if (caption.IndexOf("Microsoft Flight Simulator") > -1) // MSFS main game window
return null;
else
panelInfo.PanelType = PanelType.BuiltInPopout;
return panelInfo;
}
else // For MSFS Touch Panel window
{
var caption = PInvoke.GetWindowText(hwnd);
var panelInfo = new PanelConfig();
panelInfo.PanelHandle = hwnd;
panelInfo.PanelName = caption;
if (caption.IndexOf("Touch Panel |") > -1)
{
panelInfo.PanelType = PanelType.MSFSTouchPanel;
return panelInfo;
}
else
return null;
}
}
private Point AnalyzeMergedWindows(IntPtr hwnd)
{
var sourceImage = ImageOperation.TakeScreenShot(hwnd);
if (sourceImage == null)
return new Point(0, 0);
Rectangle rectangle;
PInvoke.GetClientRect(hwnd, out rectangle);
var panelMenubarTop = GetPanelMenubarTop(sourceImage, rectangle);
if (panelMenubarTop > sourceImage.Height)
return Point.Empty;
var panelMenubarBottom = GetPanelMenubarBottom(sourceImage, rectangle);
if (panelMenubarBottom > sourceImage.Height)
return Point.Empty;
var panelsStartingLeft = GetPanelMenubarStartingLeft(sourceImage, rectangle, panelMenubarTop + 5);
// The center of magnifying glass icon is around (3.2 x height of menubar) to the right of the panel menubar starting left
// But need to use higher number here to click the left side of magnifying glass icon because on some panel, the ratio is smaller
var menubarHeight = panelMenubarBottom - panelMenubarTop;
var magnifyingIconXCoor = panelsStartingLeft - Convert.ToInt32(menubarHeight * 3.2); // ToDo: play around with this multiplier to find the best for all resolutions
var magnifyingIconYCoor = panelMenubarTop + Convert.ToInt32(menubarHeight / 2);
return new Point(magnifyingIconXCoor, magnifyingIconYCoor);
}
private int GetPanelMenubarTop(Bitmap sourceImage, Rectangle rectangle)
{
// 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 left = Convert.ToInt32((rectangle.Width) * 0.70); // look at around 70% from the left
var top = sourceImage.Height - rectangle.Height;
if (top < 0 || left < 0)
return -1;
unsafe
{
var stripData = sourceImage.LockBits(new Rectangle(left, top, 1, rectangle.Height), 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;
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)
{
sourceImage.UnlockBits(stripData);
return y + top;
}
}
}
sourceImage.UnlockBits(stripData);
}
return -1;
}
private int GetPanelMenubarBottom(Bitmap sourceImage, Rectangle rectangle)
{
// Get a snippet of 1 pixel wide vertical strip of windows. We will choose the strip about 70% from the left of the window
var left = Convert.ToInt32((rectangle.Width) * 0.7); // look at around 70% from the left
var top = sourceImage.Height - rectangle.Height;
if (top < 0 || left < 0)
return -1;
unsafe
{
var stripData = sourceImage.LockBits(new Rectangle(left, top, 1, rectangle.Height), 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;
int menubarBottom = -1;
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 > 250 && green > 250 && blue > 250) // allows the color to be a little off white (ie. Fenix A30 EFB)
{
// found the top of menu bar
menubarBottom = y + top;
}
else if (menubarBottom > -1) /// it is no longer white in color, we hit menubar bottom
{
sourceImage.UnlockBits(stripData);
return menubarBottom;
}
}
}
sourceImage.UnlockBits(stripData);
}
return -1;
}
private int GetPanelMenubarStartingLeft(Bitmap sourceImage, Rectangle rectangle, int top)
{
unsafe
{
var stripData = sourceImage.LockBits(new Rectangle(0, top, rectangle.Width, 1), ImageLockMode.ReadWrite, sourceImage.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(stripData.PixelFormat) / 8;
int widthInPixels = stripData.Width;
int heightInBytes = stripData.Height * bytesPerPixel;
byte* ptrFirstPixel = (byte*)stripData.Scan0;
for (int x = 0; x < widthInPixels; x++)
{
byte* currentLine = ptrFirstPixel - (x * bytesPerPixel);
for (int y = 0; y < heightInBytes; y = y + bytesPerPixel)
{
int red = currentLine[y + 2];
int green = currentLine[y + 1];
int blue = currentLine[y];
if (red > 250 && green > 250 && blue > 250) // allows the color to be a little off white (ie. Fenix A30 EFB)
{
sourceImage.UnlockBits(stripData);
return sourceImage.Width - x;
}
}
}
sourceImage.UnlockBits(stripData);
}
return -1;
}
}
}