diff --git a/FsConnector/ActionEvent.cs b/FsConnector/ActionEvent.cs
new file mode 100644
index 0000000..afc8ed8
--- /dev/null
+++ b/FsConnector/ActionEvent.cs
@@ -0,0 +1,16 @@
+namespace MSFSPopoutPanelManager.FsConnector
+{
+ public enum ActionEvent
+ {
+ KEY_MASTER_BATTERY_SET,
+ KEY_ALTERNATOR_ON,
+ KEY_ALTERNATOR_OFF,
+ KEY_ALTERNATOR_SET,
+
+ KEY_AVIONICS_MASTER_SET,
+ KEY_AVIONICS_MASTER_1_ON,
+ KEY_AVIONICS_MASTER_2_ON,
+ KEY_AVIONICS_MASTER_1_OFF,
+ KEY_AVIONICS_MASTER_2_OFF
+ }
+}
diff --git a/FsConnector/DataDefinition.cs b/FsConnector/DataDefinition.cs
new file mode 100644
index 0000000..8ca0876
--- /dev/null
+++ b/FsConnector/DataDefinition.cs
@@ -0,0 +1,20 @@
+using Microsoft.FlightSimulator.SimConnect;
+using System;
+using System.Collections.Generic;
+
+namespace MSFSPopoutPanelManager.FsConnector
+{
+ public class DataDefinition
+ {
+ public static List<(string PropName, string SimConnectName, string SimConnectUnit, SIMCONNECT_DATATYPE SimConnectDataType, Type ObjectType)> GetDefinition()
+ {
+ var def = new List<(string, string, string, SIMCONNECT_DATATYPE, Type)>
+ {
+ ("Title", "Title", null, SIMCONNECT_DATATYPE.STRING256, typeof(string)),
+ ("ElectricalMasterBattery", "ELECTRICAL MASTER BATTERY", "Bool", SIMCONNECT_DATATYPE.FLOAT64, typeof(bool))
+ };
+
+ return def;
+ }
+ }
+}
diff --git a/FsConnector/Enums.cs b/FsConnector/Enums.cs
new file mode 100644
index 0000000..80607e9
--- /dev/null
+++ b/FsConnector/Enums.cs
@@ -0,0 +1,28 @@
+
+namespace MSFSPopoutPanelManager.FsConnector
+{
+ public enum SimConnectDefinition
+ {
+ SimConnectDataStruct
+ }
+
+ public enum NotificationGroup
+ {
+ GROUP0,
+ }
+
+ public enum DataRequest
+ {
+ REQUEST_1
+ }
+
+ public enum SimConnectSystemEvent
+ {
+ FOURSECS,
+ SIMSTART,
+ SIMSTOP,
+ FLIGHTLOADED,
+ VIEW,
+ PAUSED
+ };
+}
diff --git a/FsConnector/FsConnector.csproj b/FsConnector/FsConnector.csproj
new file mode 100644
index 0000000..221ee28
--- /dev/null
+++ b/FsConnector/FsConnector.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net5.0-windows
+ FsConnector
+ MSFS 2020 Popout Panel Manager FsConnector
+ MSFS 2020 Popout Panel Manager FsConnector
+ 3.2.0
+ Stanley Kwok
+ Stanley Kwok
+ Stanley Kwok 2021
+ https://github.com/hawkeye-stan/msfs-popout-panel-manager
+ MSFSPopoutPanelManager.FsConnector
+ x64;AnyCPU
+
+
+
+
+
+
+
+
+ Resources\Managed\Microsoft.FlightSimulator.SimConnect.dll
+
+
+
+
+
+ PreserveNewest
+ SimConnect.dll
+
+
+
+
diff --git a/FsConnector/Resources/Managed/Microsoft.FlightSimulator.SimConnect.dll b/FsConnector/Resources/Managed/Microsoft.FlightSimulator.SimConnect.dll
new file mode 100644
index 0000000..09b2410
Binary files /dev/null and b/FsConnector/Resources/Managed/Microsoft.FlightSimulator.SimConnect.dll differ
diff --git a/FsConnector/Resources/SimConnect.dll b/FsConnector/Resources/SimConnect.dll
new file mode 100644
index 0000000..da40b73
Binary files /dev/null and b/FsConnector/Resources/SimConnect.dll differ
diff --git a/FsConnector/SimConnectStruct.cs b/FsConnector/SimConnectStruct.cs
new file mode 100644
index 0000000..fa5bc60
--- /dev/null
+++ b/FsConnector/SimConnectStruct.cs
@@ -0,0 +1,24 @@
+
+using System.Runtime.InteropServices;
+
+namespace MSFSPopoutPanelManager.FsConnector
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public class SimConnectStruct
+ {
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x100)]
+ public string Prop01;
+
+ public double Prop02;
+ public double Prop03;
+ public double Prop04;
+ public double Prop05;
+ public double Prop06;
+ public double Prop07;
+ public double Prop08;
+ public double Prop09;
+ public double Prop10;
+
+ // Add more as DataDefinition grows
+ }
+}
diff --git a/FsConnector/SimConnector.cs b/FsConnector/SimConnector.cs
new file mode 100644
index 0000000..0f03cd3
--- /dev/null
+++ b/FsConnector/SimConnector.cs
@@ -0,0 +1,194 @@
+using Microsoft.FlightSimulator.SimConnect;
+using MSFSPopoutPanelManager.Shared;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Dynamic;
+using System.Runtime.InteropServices;
+
+
+namespace MSFSPopoutPanelManager.FsConnector
+{
+ public class SimConnector
+ {
+ private const int MSFS_CONNECTION_RETRY_TIMEOUT = 1000; // timeout to retry connection to MSFS via Simconnect in milliseconds
+ private const int WM_USER_SIMCONNECT = 0x402;
+ private SimConnect _simConnect;
+ private System.Timers.Timer _timer;
+
+ public event EventHandler> OnReceivedData;
+ public event EventHandler OnConnected;
+ public event EventHandler OnDisconnected;
+ public event EventHandler> OnReceiveSystemEvent;
+
+ public dynamic SimData { get; set; }
+
+ public void Start()
+ {
+ _timer = new System.Timers.Timer();
+ _timer.Interval = MSFS_CONNECTION_RETRY_TIMEOUT;
+ _timer.Enabled = true;
+ _timer.Elapsed += (source, e) =>
+ {
+ try
+ {
+ if (_simConnect == null)
+ {
+ _simConnect = new SimConnect("MSFS Pop Out Panel Manager", Process.GetCurrentProcess().MainWindowHandle, WM_USER_SIMCONNECT, null, 0);
+
+
+ _simConnect.OnRecvQuit += HandleOnRecvQuit;
+ _simConnect.OnRecvException += HandleOnRecvException;
+ _simConnect.OnRecvSimobjectDataBytype += HandleOnRecvSimobjectDataBytype;
+ _simConnect.OnRecvEvent += HandleOnReceiveEvent;
+
+ _simConnect.SubscribeToSystemEvent(SimConnectSystemEvent.SIMSTART, "SimStart");
+ _simConnect.SubscribeToSystemEvent(SimConnectSystemEvent.SIMSTOP, "SimStop");
+ _simConnect.SubscribeToSystemEvent(SimConnectSystemEvent.VIEW, "View");
+
+ // Setup SimConnect data structure definition using SimConnectStruct and SimConnect data definitions
+ var definitions = DataDefinition.GetDefinition();
+ foreach (var (PropName, SimConnectName, SimConnectUnit, SimConnectDataType, ObjectType) in definitions)
+ _simConnect.AddToDataDefinition(SimConnectDefinition.SimConnectDataStruct, SimConnectName, SimConnectUnit, SimConnectDataType, 0.0f, SimConnect.SIMCONNECT_UNUSED);
+ _simConnect.RegisterDataDefineStruct(SimConnectDefinition.SimConnectDataStruct);
+
+ // Setup SimEvent mapping
+ foreach (var item in Enum.GetValues(typeof(ActionEvent)))
+ {
+ if (item.ToString().StartsWith("KEY_"))
+ _simConnect.MapClientEventToSimEvent((ActionEvent)item, item.ToString()[4..]);
+ }
+
+ _timer.Enabled = false;
+
+ System.Threading.Thread.Sleep(2000);
+
+ Debug.WriteLine("SimConnect is connected");
+
+ OnConnected?.Invoke(this, null);
+ }
+ }
+ catch (COMException)
+ {
+ // handle SimConnect instantiation error when MSFS is not connected
+ }
+ };
+ }
+
+ public void Stop()
+ {
+ _timer.Enabled = false;
+ _simConnect = null;
+ }
+
+ public void StopAndReconnect()
+ {
+ _simConnect = null;
+ _timer.Enabled = true;
+ }
+
+ public void RequestData()
+ {
+ if (_simConnect != null)
+ try
+ {
+ _simConnect.RequestDataOnSimObjectType(DataRequest.REQUEST_1, SimConnectDefinition.SimConnectDataStruct, 0, SIMCONNECT_SIMOBJECT_TYPE.USER);
+ }
+ catch (Exception ex)
+ {
+ if (ex.Message != "0xC00000B0")
+ {
+ Debug.WriteLine($"SimConnect request data exception: {ex.Message}");
+ StopAndReconnect();
+ OnDisconnected?.Invoke(this, null);
+ }
+ }
+ }
+
+ public void ReceiveMessage()
+ {
+ if (_simConnect != null)
+ try
+ {
+ _simConnect.ReceiveMessage();
+ }
+ catch (Exception ex)
+ {
+ if (ex.Message != "0xC00000B0")
+ Debug.WriteLine($"SimConnect receive message exception: {ex.Message}");
+ }
+ }
+
+ public void TransmitActionEvent(ActionEvent eventID, uint data)
+ {
+ if (_simConnect != null)
+ {
+ try
+ {
+ _simConnect.TransmitClientEvent(0U, eventID, data, NotificationGroup.GROUP0, SIMCONNECT_EVENT_FLAG.GROUPID_IS_PRIORITY);
+ }
+ catch (Exception ex)
+ {
+ var eventName = eventID.ToString()[4..]; // trim out KEY_ prefix
+ Debug.WriteLine($"SimConnect transmit event exception: EventName: {eventName} - {ex.Message}");
+ }
+ }
+ }
+
+ private void HandleOnRecvQuit(SimConnect sender, SIMCONNECT_RECV data)
+ {
+ OnDisconnected?.Invoke(this, null);
+
+ // Try to reconnect again
+ _timer.Enabled = true;
+ }
+
+ private void HandleOnRecvException(SimConnect sender, SIMCONNECT_RECV_EXCEPTION data)
+ {
+ var exception = (SIMCONNECT_EXCEPTION)data.dwException;
+
+ if (exception != SIMCONNECT_EXCEPTION.NAME_UNRECOGNIZED && exception != SIMCONNECT_EXCEPTION.EVENT_ID_DUPLICATE)
+ {
+ Debug.WriteLine($"MSFS Error - {exception}");
+ }
+ }
+
+ private void HandleOnRecvSimobjectDataBytype(SimConnect sender, SIMCONNECT_RECV_SIMOBJECT_DATA_BYTYPE data)
+ {
+ if (data.dwRequestID == 0)
+ {
+ try
+ {
+ var simConnectStruct = (SimConnectStruct)data.dwData[0];
+ var simConnectStructFields = typeof(SimConnectStruct).GetFields();
+ var simData = new ExpandoObject();
+
+ var definition = DataDefinition.GetDefinition();
+ int i = 0;
+ foreach (var item in definition)
+ {
+ simData.TryAdd(item.PropName, Convert.ChangeType(simConnectStructFields[i++].GetValue(simConnectStruct), item.ObjectType));
+ }
+ SimData = simData;
+
+ OnReceivedData?.Invoke(this, new EventArgs(simData));
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"SimConnect receive data exception: {ex.Message}");
+ }
+ }
+ }
+
+ private void HandleOnReceiveEvent(SimConnect sender, SIMCONNECT_RECV_EVENT data)
+ {
+ var systemEvent = ((SimConnectSystemEvent)data.uEventID);
+
+ // Only look at VIEW for cockpit view during loading of flight (dwData = 2)
+ if (systemEvent == SimConnectSystemEvent.VIEW && data.dwData != 2)
+ return;
+
+ OnReceiveSystemEvent?.Invoke(this, new EventArgs(systemEvent));
+ }
+ }
+}
diff --git a/MSFSPopoutPanelManager.csproj b/MSFSPopoutPanelManager.csproj
deleted file mode 100644
index 9e428b9..0000000
--- a/MSFSPopoutPanelManager.csproj
+++ /dev/null
@@ -1,87 +0,0 @@
-
-
-
- WinExe
- net5.0-windows
- true
- x64;AnyCPU
- 3.1.0
- MSFSPopoutPanelManager
- MSFSPopoutPanelManager
- WindowManager.ico
- Stanley Kwok
- MSFS 2020 Popout Panel Manager
- MSFS 2020 Popout Panel Manager
- true
- 3.1.0.0
- 3.1.0.0
-
-
-
-
-
- Stanley Kwok 2021
- https://github.com/hawkeye-stan/msfs-popout-panel-manager
-
-
-
- false
- embedded
- true
-
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
-
-
-
-
- Always
-
-
- Always
-
-
-
-
- Form
-
-
-
-
- ResXFileCodeGenerator
- Resources.Designer.cs
-
-
-
-
-
-
\ No newline at end of file
diff --git a/MSFSPopoutPanelManager.sln b/MSFSPopoutPanelManager.sln
index 443d5cf..7a15d11 100644
--- a/MSFSPopoutPanelManager.sln
+++ b/MSFSPopoutPanelManager.sln
@@ -1,9 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.31919.166
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.32002.261
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSFSPopoutPanelManager", "MSFSPopoutPanelManager.csproj", "{1E89B7B3-DBD9-4644-A0EB-26924317DD83}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfApp", "WpfApp\WpfApp.csproj", "{54712A0A-B344-45E4-85C4-0A913305A0E6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Model", "Model\Model.csproj", "{4A778C1A-3782-4312-842D-33AA58A9D6D4}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "Shared\Shared.csproj", "{4BDDE1F9-FBDD-479A-B88E-B27D0513C046}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Provider", "Provider\Provider.csproj", "{933E7D03-883D-4970-9AD7-7A2B5D6C3671}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FsConnector", "FsConnector\FsConnector.csproj", "{023426F4-9FD2-4198-B9F4-83F0B55B88FC}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{15FC98CD-0A69-437B-A5E5-67D025DB5CDC}"
+ ProjectSection(SolutionItems) = preProject
+ .gitignore = .gitignore
+ LICENSE = LICENSE
+ README.md = README.md
+ VERSION.md = VERSION.md
+ EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -11,10 +27,26 @@ Global
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {1E89B7B3-DBD9-4644-A0EB-26924317DD83}.Debug|x64.ActiveCfg = Debug|x64
- {1E89B7B3-DBD9-4644-A0EB-26924317DD83}.Debug|x64.Build.0 = Debug|x64
- {1E89B7B3-DBD9-4644-A0EB-26924317DD83}.Release|x64.ActiveCfg = Release|x64
- {1E89B7B3-DBD9-4644-A0EB-26924317DD83}.Release|x64.Build.0 = Release|x64
+ {54712A0A-B344-45E4-85C4-0A913305A0E6}.Debug|x64.ActiveCfg = Debug|x64
+ {54712A0A-B344-45E4-85C4-0A913305A0E6}.Debug|x64.Build.0 = Debug|x64
+ {54712A0A-B344-45E4-85C4-0A913305A0E6}.Release|x64.ActiveCfg = Release|x64
+ {54712A0A-B344-45E4-85C4-0A913305A0E6}.Release|x64.Build.0 = Release|x64
+ {4A778C1A-3782-4312-842D-33AA58A9D6D4}.Debug|x64.ActiveCfg = Debug|x64
+ {4A778C1A-3782-4312-842D-33AA58A9D6D4}.Debug|x64.Build.0 = Debug|x64
+ {4A778C1A-3782-4312-842D-33AA58A9D6D4}.Release|x64.ActiveCfg = Release|x64
+ {4A778C1A-3782-4312-842D-33AA58A9D6D4}.Release|x64.Build.0 = Release|x64
+ {4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.Debug|x64.ActiveCfg = Debug|x64
+ {4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.Debug|x64.Build.0 = Debug|x64
+ {4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.Release|x64.ActiveCfg = Release|x64
+ {4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.Release|x64.Build.0 = Release|x64
+ {933E7D03-883D-4970-9AD7-7A2B5D6C3671}.Debug|x64.ActiveCfg = Debug|x64
+ {933E7D03-883D-4970-9AD7-7A2B5D6C3671}.Debug|x64.Build.0 = Debug|x64
+ {933E7D03-883D-4970-9AD7-7A2B5D6C3671}.Release|x64.ActiveCfg = Release|x64
+ {933E7D03-883D-4970-9AD7-7A2B5D6C3671}.Release|x64.Build.0 = Release|x64
+ {023426F4-9FD2-4198-B9F4-83F0B55B88FC}.Debug|x64.ActiveCfg = Debug|x64
+ {023426F4-9FD2-4198-B9F4-83F0B55B88FC}.Debug|x64.Build.0 = Debug|x64
+ {023426F4-9FD2-4198-B9F4-83F0B55B88FC}.Release|x64.ActiveCfg = Release|x64
+ {023426F4-9FD2-4198-B9F4-83F0B55B88FC}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Shared/Autostart.cs b/Model/AppAutoStart.cs
similarity index 98%
rename from Shared/Autostart.cs
rename to Model/AppAutoStart.cs
index 8352ca0..202255f 100644
--- a/Shared/Autostart.cs
+++ b/Model/AppAutoStart.cs
@@ -5,9 +5,9 @@ using System.Text;
using System.Xml;
using System.Xml.Serialization;
-namespace MSFSPopoutPanelManager.Shared
+namespace MSFSPopoutPanelManager.Model
{
- public class Autostart
+ public class AppAutoStart
{
public static void Activate()
{
diff --git a/Model/AppSetting.cs b/Model/AppSetting.cs
new file mode 100644
index 0000000..719bf8a
--- /dev/null
+++ b/Model/AppSetting.cs
@@ -0,0 +1,155 @@
+using MSFSPopoutPanelManager.Shared;
+using Newtonsoft.Json;
+using System;
+using System.ComponentModel;
+using System.IO;
+
+namespace MSFSPopoutPanelManager.Model
+{
+ public class AppSetting : INotifyPropertyChanged
+ {
+ private const string APP_SETTING_DATA_FILENAME = "appsettingdata.json";
+
+ private bool _saveEnabled;
+
+ // Using PropertyChanged.Fody
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public event EventHandler> AlwaysOnTopChanged;
+ public event EventHandler> AutoPopOutPanelsChanged;
+
+ public AppSetting()
+ {
+ // Set defaults
+ MinimizeToTray = false;
+ AlwaysOnTop = true;
+ UseAutoPanning = true;
+ StartMinimized = false;
+ IncludeBuiltInPanel = false;
+ AutoPopOutPanels = false;
+ AutoPopOutPanelsWaitDelay = new AutoPopOutPanelsWaitDelay();
+ }
+
+ public void Load()
+ {
+ var appSetting = ReadAppSetting();
+ this.MinimizeToTray = appSetting.MinimizeToTray;
+ this.AlwaysOnTop = appSetting.AlwaysOnTop;
+ this.UseAutoPanning = appSetting.UseAutoPanning;
+ this.StartMinimized = appSetting.StartMinimized;
+ this.IncludeBuiltInPanel = appSetting.IncludeBuiltInPanel;
+ this.AutoPopOutPanels = appSetting.AutoPopOutPanels;
+ this.AutoPopOutPanelsWaitDelay = appSetting.AutoPopOutPanelsWaitDelay;
+
+ _saveEnabled = true;
+ }
+
+ public void OnPropertyChanged(string propertyName, object before, object after)
+ {
+ // Automatic save data
+ if (_saveEnabled && propertyName != "AutoStart" && before != after)
+ WriteAppSetting(this);
+
+ switch (propertyName)
+ {
+ case "AlwaysOnTop":
+ AlwaysOnTopChanged?.Invoke(this, new EventArgs((bool)after));
+ break;
+ case "AutoPopOutPanels":
+ AutoPopOutPanelsChanged?.Invoke(this, new EventArgs((bool)after));
+ break;
+ }
+ }
+
+ //[OnDeserialized]
+ //private void OnDeserialized(StreamingContext context)
+ //{
+ // // Allow save data
+ // _saveEnabled = true;
+ //}
+
+ public bool MinimizeToTray { get; set; }
+
+ public bool AlwaysOnTop { get; set; }
+
+ public bool UseAutoPanning { get; set; }
+
+ public bool StartMinimized { get; set; }
+
+ public bool IncludeBuiltInPanel { get; set; }
+
+ public bool AutoPopOutPanels { get; set; }
+
+ public AutoPopOutPanelsWaitDelay AutoPopOutPanelsWaitDelay { get; set; }
+
+ [JsonIgnore]
+ public bool AutoStart
+ {
+ get
+ {
+ return AppAutoStart.CheckIsAutoStart();
+ }
+ set
+ {
+ if (value)
+ AppAutoStart.Activate();
+ else
+ AppAutoStart.Deactivate();
+ }
+ }
+
+ public AppSetting ReadAppSetting()
+ {
+ try
+ {
+ using (StreamReader reader = new StreamReader(Path.Combine(FileIo.GetUserDataFilePath(), APP_SETTING_DATA_FILENAME)))
+ {
+ return JsonConvert.DeserializeObject(reader.ReadToEnd());
+ }
+ }
+ catch (Exception ex)
+ {
+ // if file does not exist, write default data
+ var appSetting = new AppSetting();
+ WriteAppSetting(appSetting);
+ return appSetting;
+ }
+ }
+
+ public void WriteAppSetting(AppSetting appSetting)
+ {
+ try
+ {
+ var userProfilePath = FileIo.GetUserDataFilePath();
+ if (!Directory.Exists(userProfilePath))
+ Directory.CreateDirectory(userProfilePath);
+
+ using (StreamWriter file = File.CreateText(Path.Combine(userProfilePath, APP_SETTING_DATA_FILENAME)))
+ {
+ JsonSerializer serializer = new JsonSerializer();
+ serializer.Serialize(file, appSetting);
+ }
+ }
+ catch
+ {
+ Logger.LogStatus($"Unable to write app setting data file: {APP_SETTING_DATA_FILENAME}", StatusMessageType.Error);
+ }
+ }
+ }
+
+ public class AutoPopOutPanelsWaitDelay
+ {
+ public AutoPopOutPanelsWaitDelay()
+ {
+ ReadyToFlyButton = 2;
+ InitialCockpitView = 2;
+ InstrumentationPowerOn = 2;
+ }
+
+ public int ReadyToFlyButton { get; set; }
+
+ public int InitialCockpitView { get; set; }
+
+ public int InstrumentationPowerOn { get; set; }
+ }
+}
diff --git a/Model/FodyWeavers.xml b/Model/FodyWeavers.xml
new file mode 100644
index 0000000..d5abfed
--- /dev/null
+++ b/Model/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/Model/FodyWeavers.xsd b/Model/FodyWeavers.xsd
new file mode 100644
index 0000000..69dbe48
--- /dev/null
+++ b/Model/FodyWeavers.xsd
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+ Used to control if the On_PropertyName_Changed feature is enabled.
+
+
+
+
+ Used to control if the Dependent properties feature is enabled.
+
+
+
+
+ Used to control if the IsChanged property feature is enabled.
+
+
+
+
+ Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form.
+
+
+
+
+ Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project.
+
+
+
+
+ Used to control if equality checks should use the Equals method resolved from the base class.
+
+
+
+
+ Used to control if equality checks should use the static Equals method resolved from the base class.
+
+
+
+
+ Used to turn off build warnings from this weaver.
+
+
+
+
+ Used to turn off build warnings about mismatched On_PropertyName_Changed methods.
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/Model/Model.csproj b/Model/Model.csproj
new file mode 100644
index 0000000..21a5390
--- /dev/null
+++ b/Model/Model.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net5.0-windows
+ MSFSPopoutPanelManager.Model
+ Model
+ MSFS 2020 Popout Panel Manager Model
+ 3.2.0
+ Stanley Kwok
+ Stanley Kwok
+ Stanley Kwok 2021
+ MSFS 2020 Popout Panel Manager Model
+ https://github.com/hawkeye-stan/msfs-popout-panel-manager
+ x64;AnyCPU
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
diff --git a/Model/PanelConfig.cs b/Model/PanelConfig.cs
new file mode 100644
index 0000000..36cd4a6
--- /dev/null
+++ b/Model/PanelConfig.cs
@@ -0,0 +1,35 @@
+using Newtonsoft.Json;
+using System;
+using System.ComponentModel;
+
+namespace MSFSPopoutPanelManager.Model
+{
+ public class PanelConfig : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public int PanelIndex { get; set; }
+
+ public string PanelName { get; set; }
+
+ public PanelType PanelType { get; set; }
+
+ public int Top { get; set; }
+
+ public int Left { get; set; }
+
+ public int Width { get; set; }
+
+ public int Height { get; set; }
+
+ public bool AlwaysOnTop { get; set; }
+
+ public bool HideTitlebar { get; set; }
+
+ [JsonIgnore]
+ public bool IsCustomPopout { get { return PanelType == PanelType.CustomPopout; } }
+
+ [JsonIgnore]
+ public IntPtr PanelHandle { get; set; }
+ }
+}
diff --git a/Model/PanelConfigPropertyName.cs b/Model/PanelConfigPropertyName.cs
new file mode 100644
index 0000000..68ed9c7
--- /dev/null
+++ b/Model/PanelConfigPropertyName.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace MSFSPopoutPanelManager.Model
+{
+ public enum PanelConfigPropertyName
+ {
+ PanelName,
+ Left,
+ Top,
+ Width,
+ Height,
+ AlwaysOnTop,
+ HideTitlebar,
+ Invalid
+ }
+
+ public class PanelConfigItem
+ {
+ public int PanelIndex { get; set; }
+
+ public PanelConfigPropertyName PanelConfigProperty { get; set; }
+ }
+}
diff --git a/Model/PanelSourceCoordinate.cs b/Model/PanelSourceCoordinate.cs
new file mode 100644
index 0000000..2c47f1b
--- /dev/null
+++ b/Model/PanelSourceCoordinate.cs
@@ -0,0 +1,20 @@
+using Newtonsoft.Json;
+using System;
+using System.ComponentModel;
+
+namespace MSFSPopoutPanelManager.Model
+{
+ public class PanelSourceCoordinate : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public int PanelIndex { get; set; }
+
+ public int X { get; set; }
+
+ public int Y { get; set; }
+
+ [JsonIgnore]
+ public IntPtr PanelHandle { get; set; }
+ }
+}
diff --git a/Shared/Enums.cs b/Model/PanelType.cs
similarity index 76%
rename from Shared/Enums.cs
rename to Model/PanelType.cs
index 0604703..067d473 100644
--- a/Shared/Enums.cs
+++ b/Model/PanelType.cs
@@ -1,4 +1,4 @@
-namespace MSFSPopoutPanelManager.Shared
+namespace MSFSPopoutPanelManager.Model
{
public enum PanelType
{
diff --git a/Model/UserProfile.cs b/Model/UserProfile.cs
new file mode 100644
index 0000000..31529ff
--- /dev/null
+++ b/Model/UserProfile.cs
@@ -0,0 +1,50 @@
+using Newtonsoft.Json;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+
+namespace MSFSPopoutPanelManager.Model
+{
+ public class UserProfile : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public UserProfile()
+ {
+ PanelSourceCoordinates = new ObservableCollection();
+ PanelConfigs = new ObservableCollection();
+ IsLocked = false;
+ }
+
+ public int ProfileId { get; set; }
+
+ public string ProfileName { get; set; }
+
+ public bool IsDefaultProfile { get; set; }
+
+ public string BindingPlaneTitle { get; set; }
+
+ public bool IsLocked { get; set; }
+
+ public ObservableCollection PanelSourceCoordinates;
+
+ public ObservableCollection PanelConfigs { get; set; }
+
+ public bool PowerOnRequiredForColdStart { get; set; }
+
+ public void Reset()
+ {
+ PanelSourceCoordinates.Clear();
+ PanelConfigs.Clear();
+ IsLocked = false;
+ }
+
+ [JsonIgnore]
+ public bool IsActive { get; set; }
+
+ [JsonIgnore]
+ public bool HasBindingPlaneTitle
+ {
+ get { return !string.IsNullOrEmpty(BindingPlaneTitle); }
+ }
+ }
+}
diff --git a/Program.cs b/Program.cs
deleted file mode 100644
index 4879637..0000000
--- a/Program.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using log4net;
-using log4net.Config;
-using MSFSPopoutPanelManager.Shared;
-using MSFSPopoutPanelManager.UI;
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Reflection;
-using System.Threading;
-using System.Windows.Forms;
-
-namespace MSFSPopoutPanelManager
-{
- static class Program
- {
- private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
-
- ///
- /// The main entry point for the application.
- ///
- [STAThread]
- static void Main()
- {
- bool createNew;
- using var mutex = new Mutex(true, typeof(Program).Namespace, out createNew);
-
- var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
- XmlConfigurator.Configure(logRepository, new FileInfo("log4net.config"));
-
- if (createNew)
- {
- Application.SetHighDpiMode(HighDpiMode.SystemAware);
- Application.EnableVisualStyles();
- Application.SetCompatibleTextRenderingDefault(false);
- Application.ThreadException += new ThreadExceptionEventHandler(HandleThreadException);
- Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
- AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(UnhandledDomainException);
- Application.Run(new StartupForm());
- }
- else
- {
- var current = Process.GetCurrentProcess();
-
- foreach (var process in Process.GetProcessesByName(current.ProcessName))
- {
- if (process.Id == current.Id) continue;
- PInvoke.SetForegroundWindow(process.MainWindowHandle);
- break;
- }
- }
-
- void HandleThreadException(object sender, ThreadExceptionEventArgs e)
- {
- Log.Error(e.Exception.Message, e.Exception);
- ShowExceptionForm();
- }
-
- void UnhandledDomainException(object sender, UnhandledExceptionEventArgs e)
- {
- var exception = (Exception)e.ExceptionObject;
- Log.Error(exception.Message, exception);
-
- ShowExceptionForm();
- }
-
- void ShowExceptionForm()
- {
- var title = "Critical Error";
- var message = "Application has encountered a critical error and will be closed. Please see the file error.log for information.";
-
- using (var form = new ConfirmDialogForm(title, message, false) { StartPosition = FormStartPosition.CenterParent })
- {
- var dialogResult = form.ShowDialog();
-
- if (dialogResult == DialogResult.OK)
- {
- Application.Exit();
- }
- }
- }
- }
- }
-}
diff --git a/Provider/DiagnosticManager.cs b/Provider/DiagnosticManager.cs
new file mode 100644
index 0000000..790b181
--- /dev/null
+++ b/Provider/DiagnosticManager.cs
@@ -0,0 +1,72 @@
+using MSFSPopoutPanelManager.Shared;
+using System;
+using System.Diagnostics;
+using System.Timers;
+
+namespace MSFSPopoutPanelManager.Provider
+{
+ public class DiagnosticManager
+ {
+ public static event EventHandler> OnPollMsfsConnectionResult;
+
+ public static string GetApplicationVersion()
+ {
+ var systemAssemblyVersion = System.Reflection.Assembly.GetEntryAssembly().GetName().Version;
+ var appVersion = $"{systemAssemblyVersion.Major}.{systemAssemblyVersion.Minor}.{systemAssemblyVersion.Build}";
+ if (systemAssemblyVersion.Revision > 0)
+ appVersion += "." + systemAssemblyVersion.Revision;
+
+ return appVersion;
+ }
+
+ public static WindowProcess GetSimulatorProcess()
+ {
+ return GetProcess("FlightSimulator");
+ }
+
+ public static WindowProcess GetApplicationProcess()
+ {
+ return GetProcess("MSFSPopoutPanelManager");
+ }
+
+ public static void StartPollingMsfsConnection()
+ {
+ Timer timer = new Timer();
+ timer.Interval = 2000;
+ timer.Elapsed += (sender, e) =>
+ {
+ OnPollMsfsConnectionResult?.Invoke(null, new EventArgs(GetSimulatorProcess() != null));
+ };
+ timer.Enabled = true;
+
+ }
+
+ public static void OpenOnlineUserGuide()
+ {
+ Process.Start(new ProcessStartInfo("https://github.com/hawkeye-stan/msfs-popout-panel-manager#msfs-pop-out-panel-manager") { UseShellExecute = true });
+ }
+
+ public static void OpenOnlineLatestDownload()
+ {
+ Process.Start(new ProcessStartInfo("https://github.com/hawkeye-stan/msfs-popout-panel-manager/releases") { UseShellExecute = true });
+ }
+
+ private static WindowProcess GetProcess(string processName)
+ {
+ foreach (var process in Process.GetProcesses())
+ {
+ if (process.ProcessName == processName)
+ {
+ return new WindowProcess()
+ {
+ ProcessId = process.Id,
+ ProcessName = process.ProcessName,
+ Handle = process.MainWindowHandle
+ };
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Provider/FileManager.cs b/Provider/FileManager.cs
deleted file mode 100644
index 2893e0f..0000000
--- a/Provider/FileManager.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-using MSFSPopoutPanelManager.Shared;
-using Newtonsoft.Json;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Windows.Forms;
-
-namespace MSFSPopoutPanelManager.Provider
-{
- public class FileManager
- {
- private static string UserProfileDataPath;
- private const string APP_SETTING_DATA_FILENAME = "appsettingdata.json";
- private const string USER_PROFILE_DATA_FILENAME = "userprofiledata.json";
-
- static FileManager()
- {
- FileManager.UserProfileDataPath = Path.Combine(Application.StartupPath, "userdata");
- }
-
- public static List ReadUserProfileData()
- {
- try
- {
- using (StreamReader reader = new StreamReader(Path.Combine(UserProfileDataPath, USER_PROFILE_DATA_FILENAME)))
- {
- return JsonConvert.DeserializeObject>(reader.ReadToEnd());
- }
- }
- catch
- {
- return new List();
- }
- }
-
- public static void WriteUserProfileData(List userProfileData)
- {
- try
- {
- if (!Directory.Exists(UserProfileDataPath))
- Directory.CreateDirectory(UserProfileDataPath);
-
- using (StreamWriter file = File.CreateText(Path.Combine(UserProfileDataPath, USER_PROFILE_DATA_FILENAME)))
- {
- JsonSerializer serializer = new JsonSerializer();
- serializer.Serialize(file, userProfileData);
- }
- }
- catch
- {
- Logger.BackgroundStatus($"Unable to write user data file: {USER_PROFILE_DATA_FILENAME}", StatusMessageType.Error);
- }
- }
-
- public static AppSettingData ReadAppSettingData()
- {
- try
- {
- using (StreamReader reader = new StreamReader(Path.Combine(UserProfileDataPath, APP_SETTING_DATA_FILENAME)))
- {
- return JsonConvert.DeserializeObject(reader.ReadToEnd());
- }
- }
- catch (Exception ex)
- {
- // if file does not exist, write default data
- var appSettings = new AppSettingData();
- WriteAppSettingData(appSettings);
-
- return appSettings;
- }
- }
-
- public static void WriteAppSettingData(AppSettingData appSettingData)
- {
- try
- {
- if (!Directory.Exists(UserProfileDataPath))
- Directory.CreateDirectory(UserProfileDataPath);
-
- using (StreamWriter file = File.CreateText(Path.Combine(UserProfileDataPath, APP_SETTING_DATA_FILENAME)))
- {
- JsonSerializer serializer = new JsonSerializer();
- serializer.Serialize(file, appSettingData);
- }
- }
- catch
- {
- Logger.BackgroundStatus($"Unable to write app setting data file: {USER_PROFILE_DATA_FILENAME}", StatusMessageType.Error);
- }
- }
- }
-}
diff --git a/Shared/ImageOperation.cs b/Provider/ImageOperation.cs
similarity index 95%
rename from Shared/ImageOperation.cs
rename to Provider/ImageOperation.cs
index cb4268b..4667972 100644
--- a/Shared/ImageOperation.cs
+++ b/Provider/ImageOperation.cs
@@ -3,7 +3,7 @@ using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;
-namespace MSFSPopoutPanelManager.Shared
+namespace MSFSPopoutPanelManager.Provider
{
public class ImageOperation
{
diff --git a/Provider/InputEmulationManager.cs b/Provider/InputEmulationManager.cs
index 21d2684..9ba31b9 100644
--- a/Provider/InputEmulationManager.cs
+++ b/Provider/InputEmulationManager.cs
@@ -1,5 +1,6 @@
-using MSFSPopoutPanelManager.Shared;
-using System;
+using System;
+using System.Diagnostics;
+using System.Drawing;
using System.Threading;
namespace MSFSPopoutPanelManager.Provider
@@ -17,14 +18,6 @@ namespace MSFSPopoutPanelManager.Provider
const uint VK_SPACE = 0x20;
const uint KEY_0 = 0x30;
- public static void SendMouseToLocation(IntPtr hwnd, int x, int y)
- {
- // Move the cursor to the flight simulator screen then move the cursor into position
- //PInvoke.SetCursorPos(0, 0);
- PInvoke.SetFocus(hwnd);
- PInvoke.SetCursorPos(x, y);
- }
-
public static void LeftClick(int x, int y)
{
PInvoke.SetCursorPos(x, y);
@@ -47,13 +40,10 @@ namespace MSFSPopoutPanelManager.Provider
PInvoke.keybd_event(Convert.ToByte(VK_RMENU), 0, KEYEVENTF_KEYUP, 0);
}
- public static void CenterView(IntPtr hwnd)
+ public static void CenterView(IntPtr hwnd, int x, int y)
{
PInvoke.SetForegroundWindow(hwnd);
- Thread.Sleep(500);
-
- PInvoke.SetFocus(hwnd);
- Thread.Sleep(300);
+ LeftClick(x, y);
// First center view using Ctrl-Space
PInvoke.keybd_event(Convert.ToByte(VK_LCONTROL), 0, KEYEVENTF_KEYDOWN, 0);
@@ -80,7 +70,6 @@ namespace MSFSPopoutPanelManager.Provider
PInvoke.keybd_event(Convert.ToByte(KEY_0), 0, KEYEVENTF_KEYUP, 0);
PInvoke.keybd_event(Convert.ToByte(VK_LMENU), 0, KEYEVENTF_KEYUP, 0);
PInvoke.keybd_event(Convert.ToByte(VK_LCONTROL), 0, KEYEVENTF_KEYUP, 0);
-
}
public static void LoadCustomViewZero(IntPtr hwnd)
@@ -106,5 +95,48 @@ namespace MSFSPopoutPanelManager.Provider
PInvoke.keybd_event(Convert.ToByte(KEY_0), 0, KEYEVENTF_KEYUP, 0);
PInvoke.keybd_event(Convert.ToByte(VK_LMENU), 0, KEYEVENTF_KEYUP, 0);
}
+
+ public static void LeftClickReadyToFly()
+ {
+ var simualatorProcess = DiagnosticManager.GetSimulatorProcess();
+ if (simualatorProcess != null)
+ {
+ var hwnd = simualatorProcess.Handle;
+
+ PInvoke.SetForegroundWindow(hwnd);
+ Thread.Sleep(500);
+
+ Rectangle rectangle;
+ PInvoke.GetWindowRect(hwnd, out rectangle);
+
+ Rectangle clientRectangle;
+ PInvoke.GetClientRect(hwnd, out clientRectangle);
+
+ // For windows mode
+ // The "Ready to Fly" button is at about 93% width, 91.3% height at the lower right corner of game window
+ var x = Convert.ToInt32(rectangle.X + (clientRectangle.Width + 8) * 0.93); // with 8 pixel adjustment
+ var y = Convert.ToInt32(rectangle.Y + (clientRectangle.Height + 39) * 0.915); // with 39 pixel adjustment
+
+ LeftClick(x, y); // set focus to game app
+ Thread.Sleep(250);
+ LeftClick(x, y);
+ Thread.Sleep(250);
+
+
+ Debug.WriteLine($"Windows Mode 'Ready to Fly' button coordinate: {x}, {y}");
+
+ // For full screen mode
+ x = Convert.ToInt32(rectangle.X + (clientRectangle.Width) * 0.93);
+ y = Convert.ToInt32(rectangle.Y + (clientRectangle.Height) * 0.915);
+
+ LeftClick(x, y); // set focus to game app
+ Thread.Sleep(250);
+ LeftClick(x, y);
+ Thread.Sleep(250);
+
+ Debug.WriteLine($"Full Screen Mode 'Ready to Fly' button coordinate: {x} , {y}");
+
+ }
+ }
}
}
\ No newline at end of file
diff --git a/Provider/InputHookManager.cs b/Provider/InputHookManager.cs
new file mode 100644
index 0000000..c459700
--- /dev/null
+++ b/Provider/InputHookManager.cs
@@ -0,0 +1,73 @@
+using Gma.System.MouseKeyHook;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace MSFSPopoutPanelManager.Provider
+{
+ public class InputHookManager
+ {
+ private const string CTRL_KEY = "Control";
+ private const string SHIFT_KEY = "Shift";
+
+ private static IKeyboardMouseEvents _mouseHook;
+
+ public static bool SubscribeToPanelSelectionEvent { get; set; }
+ public static bool SubscribeToStartPopOutEvent { get; set; }
+
+ public static event EventHandler OnPanelSelectionCompleted;
+ public static event EventHandler OnPanelSelectionAdded;
+ public static event EventHandler OnPanelSelectionRemoved;
+ public static event EventHandler OnStartPopout;
+
+ public static void StartHook()
+ {
+ if (_mouseHook == null)
+ {
+ _mouseHook = Hook.GlobalEvents();
+
+ _mouseHook.OnCombination(new Dictionary
+ {
+ {Combination.FromString("Control+Alt+P"), () => { if(SubscribeToStartPopOutEvent) OnStartPopout?.Invoke(null, null); }}
+ });
+
+ _mouseHook.MouseDownExt += HandleMouseHookMouseDownExt;
+ }
+ }
+
+ public static void EndHook()
+ {
+ if (_mouseHook == null)
+ {
+ _mouseHook.MouseDownExt -= HandleMouseHookMouseDownExt;
+ _mouseHook.Dispose();
+ }
+ }
+
+ private static void HandleMouseHookMouseDownExt(object sender, MouseEventExtArgs e)
+ {
+ if (_mouseHook == null || !SubscribeToPanelSelectionEvent)
+ return;
+
+ if (e.Button == MouseButtons.Left)
+ {
+ var ctrlPressed = Control.ModifierKeys.ToString() == CTRL_KEY;
+ var shiftPressed = Control.ModifierKeys.ToString() == SHIFT_KEY;
+
+ if (ctrlPressed)
+ {
+ OnPanelSelectionCompleted?.Invoke(null, null);
+ }
+ else if (shiftPressed)
+ {
+ OnPanelSelectionRemoved?.Invoke(null, null);
+ }
+ else if (!shiftPressed)
+ {
+ OnPanelSelectionAdded?.Invoke(null, new Point(e.X, e.Y));
+ }
+ }
+ }
+ }
+}
diff --git a/Shared/PInvoke.cs b/Provider/PInvoke.cs
similarity index 90%
rename from Shared/PInvoke.cs
rename to Provider/PInvoke.cs
index 507f78e..9896187 100644
--- a/Shared/PInvoke.cs
+++ b/Provider/PInvoke.cs
@@ -3,13 +3,15 @@ using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
-namespace MSFSPopoutPanelManager.Shared
+namespace MSFSPopoutPanelManager.Provider
{
public static class PInvokeConstant
{
public const int SW_SHOWNORMAL = 1;
public const int SW_SHOWMINIMIZED = 2;
public const int SW_SHOWMAXIMIZED = 3;
+ public const int SW_SHOW = 5;
+ public const int SW_SHOWDEFAULT = 10;
public const int SW_NORMAL = 1;
public const int SW_MINIMIZE = 6;
public const int SW_RESTORE = 9;
@@ -33,12 +35,15 @@ namespace MSFSPopoutPanelManager.Shared
public const uint WM_CLOSE = 0x0010;
public const int WINEVENT_OUTOFCONTEXT = 0;
}
-
+
public class PInvoke
{
[DllImport("user32")]
public static extern int EnumWindows(CallBack x, int y);
+ [DllImport("user32")]
+ private static extern bool EnumChildWindows(IntPtr window, CallBack callback, IntPtr lParam);
+
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int GetClassName(IntPtr hWnd, StringBuilder strPtrClassName, Int32 nMaxCount);
@@ -97,11 +102,14 @@ namespace MSFSPopoutPanelManager.Shared
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
+ [DllImport("user32.dll")]
+ public static extern bool ShowWindowAsync(HandleRef hWnd, int nCmdShow);
+
[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);
+ public static extern IntPtr SetWindowPos(IntPtr hWnd, IntPtr 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);
diff --git a/Provider/PanelConfigurationManager.cs b/Provider/PanelConfigurationManager.cs
new file mode 100644
index 0000000..edc9496
--- /dev/null
+++ b/Provider/PanelConfigurationManager.cs
@@ -0,0 +1,222 @@
+using MSFSPopoutPanelManager.Model;
+using System;
+using System.Diagnostics;
+using System.Drawing;
+using System.Linq;
+
+namespace MSFSPopoutPanelManager.Provider
+{
+ public class PanelConfigurationManager
+ {
+ private UserProfileManager _userProfileManager;
+ private IntPtr _winEventHook;
+ private static PInvoke.WinEventProc _winEvent; // keep this as static to prevent garbage collect or the app will crash
+ private Rectangle _lastWindowRectangle;
+
+ public UserProfile UserProfile { get; set; }
+
+ public bool AllowEdit { get; set; }
+
+ public PanelConfigurationManager(UserProfileManager userProfileManager)
+ {
+ _userProfileManager = userProfileManager;
+ _winEvent = new PInvoke.WinEventProc(EventCallback);
+ AllowEdit = true;
+ }
+
+ public void HookWinEvent()
+ {
+ // Setup panel config event hooks
+ _winEventHook = PInvoke.SetWinEventHook(PInvokeConstant.EVENT_SYSTEM_MOVESIZEEND, PInvokeConstant.EVENT_OBJECT_LOCATIONCHANGE, DiagnosticManager.GetApplicationProcess().Handle, _winEvent, 0, 0, PInvokeConstant.WINEVENT_OUTOFCONTEXT);
+ }
+
+ public void UnhookWinEvent()
+ {
+ // Unhook all Win API events
+ PInvoke.UnhookWinEvent(_winEventHook);
+ }
+
+ public void LockPanelsUpdated()
+ {
+ UserProfile.IsLocked = !UserProfile.IsLocked;
+ _userProfileManager.WriteUserProfiles();
+ }
+
+ public void PanelConfigPropertyUpdated(PanelConfigItem panelConfigItem)
+ {
+ if (!AllowEdit || UserProfile.IsLocked)
+ return;
+
+ var panelConfig = UserProfile.PanelConfigs.ToList().Find(p => p.PanelIndex == panelConfigItem.PanelIndex);
+
+ if (panelConfig != null)
+ {
+ switch (panelConfigItem.PanelConfigProperty)
+ {
+ case PanelConfigPropertyName.PanelName:
+ var name = panelConfig.PanelName;
+ if (name.IndexOf("(Custom)") == -1)
+ name = name + " (Custom)";
+ PInvoke.SetWindowText(panelConfig.PanelHandle, name);
+ break;
+ case PanelConfigPropertyName.Left:
+ case PanelConfigPropertyName.Top:
+ PInvoke.MoveWindow(panelConfig.PanelHandle, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height, true);
+ break;
+ case PanelConfigPropertyName.Width:
+ case PanelConfigPropertyName.Height:
+ int orignalLeft = panelConfig.Left;
+ PInvoke.MoveWindow(panelConfig.PanelHandle, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height, true);
+ MSFSBugPanelShiftWorkaround(panelConfig.PanelHandle, orignalLeft, panelConfig.Top, panelConfig.Width, panelConfig.Height);
+ break;
+ case PanelConfigPropertyName.AlwaysOnTop:
+ WindowManager.ApplyAlwaysOnTop(panelConfig.PanelHandle, panelConfig.AlwaysOnTop, new Rectangle(panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height));
+ break;
+ case PanelConfigPropertyName.HideTitlebar:
+ WindowManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, panelConfig.HideTitlebar);
+ break;
+ }
+
+ _userProfileManager.WriteUserProfiles();
+ }
+ }
+
+ public void PanelConfigIncreaseDecrease(PanelConfigItem panelConfigItem, int changeAmount)
+ {
+ if (!AllowEdit || UserProfile.IsLocked || UserProfile.PanelConfigs == null || UserProfile.PanelConfigs.Count == 0)
+ return;
+
+ var index = UserProfile.PanelConfigs.ToList().FindIndex(p => p.PanelIndex == panelConfigItem.PanelIndex);
+
+ if (index > -1)
+ {
+ var panelConfig = UserProfile.PanelConfigs[index];
+
+ int orignalLeft = panelConfig.Left;
+
+ switch (panelConfigItem.PanelConfigProperty)
+ {
+ case PanelConfigPropertyName.Left:
+ PInvoke.MoveWindow(panelConfig.PanelHandle, panelConfig.Left + changeAmount, panelConfig.Top, panelConfig.Width, panelConfig.Height, false);
+ panelConfig.Left += changeAmount;
+ break;
+ case PanelConfigPropertyName.Top:
+ PInvoke.MoveWindow(panelConfig.PanelHandle, panelConfig.Left, panelConfig.Top + changeAmount, panelConfig.Width, panelConfig.Height, false);
+ panelConfig.Top += changeAmount;
+ break;
+ case PanelConfigPropertyName.Width:
+ PInvoke.MoveWindow(panelConfig.PanelHandle, panelConfig.Left, panelConfig.Top, panelConfig.Width + changeAmount, panelConfig.Height, false);
+ MSFSBugPanelShiftWorkaround(panelConfig.PanelHandle, orignalLeft, panelConfig.Top, panelConfig.Width + changeAmount, panelConfig.Height);
+ panelConfig.Width += changeAmount;
+ break;
+ case PanelConfigPropertyName.Height:
+ PInvoke.MoveWindow(panelConfig.PanelHandle, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height + changeAmount, false);
+ MSFSBugPanelShiftWorkaround(panelConfig.PanelHandle, orignalLeft, panelConfig.Top, panelConfig.Width, panelConfig.Height + changeAmount);
+ panelConfig.Height += changeAmount;
+ break;
+ default:
+ return;
+ }
+
+ _userProfileManager.WriteUserProfiles();
+ }
+ }
+
+ private void MSFSBugPanelShiftWorkaround(IntPtr handle, int originalLeft, int top, int width, int height)
+ {
+ // Fixed MSFS bug, create workaround where on 2nd or later instance of width adjustment, the panel shift to the left by itself
+ // Wait for system to catch up on panel coordinate that were just applied
+ System.Threading.Thread.Sleep(200);
+
+ Rectangle rectangle;
+ PInvoke.GetWindowRect(handle, out rectangle);
+
+ if (rectangle.Left != originalLeft)
+ PInvoke.MoveWindow(handle, originalLeft, top, width, height, false);
+ }
+
+ private void EventCallback(IntPtr hWinEventHook, uint iEvent, IntPtr hWnd, int idObject, int idChild, int dwEventThread, int dwmsEventTime)
+ {
+ PanelConfig panelConfig;
+
+ // check by priority to minimize escaping constraint
+ if (hWnd == IntPtr.Zero
+ || idObject != 0
+ || hWinEventHook != _winEventHook
+ || !AllowEdit
+ || !(iEvent == PInvokeConstant.EVENT_OBJECT_LOCATIONCHANGE || iEvent == PInvokeConstant.EVENT_SYSTEM_MOVESIZEEND)
+ || UserProfile.PanelConfigs == null || UserProfile.PanelConfigs.Count == 0)
+ {
+ return;
+ }
+
+ if(UserProfile.IsLocked)
+ {
+ panelConfig = UserProfile.PanelConfigs.FirstOrDefault(panel => panel.PanelHandle == hWnd);
+
+ if (panelConfig != null && panelConfig.PanelType == PanelType.CustomPopout)
+ {
+ // Move window back to original location if user profile is locked
+ if (iEvent == PInvokeConstant.EVENT_SYSTEM_MOVESIZEEND)
+ {
+ PInvoke.MoveWindow(panelConfig.PanelHandle, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height, false);
+ return;
+ }
+
+ if (iEvent == PInvokeConstant.EVENT_OBJECT_LOCATIONCHANGE)
+ {
+ // Detect if window is maximized, if so, save settings
+ WINDOWPLACEMENT wp = new WINDOWPLACEMENT();
+ wp.length = System.Runtime.InteropServices.Marshal.SizeOf(wp);
+ PInvoke.GetWindowPlacement(hWnd, ref wp);
+ if (wp.showCmd == PInvokeConstant.SW_SHOWMAXIMIZED || wp.showCmd == PInvokeConstant.SW_SHOWMINIMIZED || wp.showCmd == PInvokeConstant.SW_SHOWNORMAL)
+ {
+ PInvoke.ShowWindow(hWnd, PInvokeConstant.SW_RESTORE);
+ }
+ return;
+ }
+ }
+
+ return;
+ }
+
+ panelConfig = UserProfile.PanelConfigs.FirstOrDefault(panel => panel.PanelHandle == hWnd);
+
+ if (panelConfig != null)
+ {
+ switch (iEvent)
+ {
+ case PInvokeConstant.EVENT_OBJECT_LOCATIONCHANGE:
+ Rectangle winRectangle;
+ PInvoke.GetWindowRect(panelConfig.PanelHandle, out winRectangle);
+
+ if (_lastWindowRectangle == winRectangle) // ignore duplicate callback messages
+ return;
+
+ _lastWindowRectangle = winRectangle;
+ Rectangle clientRectangle;
+ PInvoke.GetClientRect(panelConfig.PanelHandle, out clientRectangle);
+
+ panelConfig.Left = winRectangle.Left;
+ panelConfig.Top = winRectangle.Top;
+ panelConfig.Width = clientRectangle.Width + 16;
+ panelConfig.Height = clientRectangle.Height + 39;
+
+ // Detect if window is maximized, if so, save settings
+ WINDOWPLACEMENT wp = new WINDOWPLACEMENT();
+ wp.length = System.Runtime.InteropServices.Marshal.SizeOf(wp);
+ PInvoke.GetWindowPlacement(hWnd, ref wp);
+ if (wp.showCmd == PInvokeConstant.SW_SHOWMAXIMIZED || wp.showCmd == PInvokeConstant.SW_SHOWMINIMIZED)
+ {
+ _userProfileManager.WriteUserProfiles();
+ }
+
+ break;
+ case PInvokeConstant.EVENT_SYSTEM_MOVESIZEEND:
+ _userProfileManager.WriteUserProfiles();
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/Provider/PopoutSeparationManager.cs b/Provider/PanelPopoutManager.cs
similarity index 72%
rename from Provider/PopoutSeparationManager.cs
rename to Provider/PanelPopoutManager.cs
index 360bada..af18450 100644
--- a/Provider/PopoutSeparationManager.cs
+++ b/Provider/PanelPopoutManager.cs
@@ -1,33 +1,54 @@
-using MSFSPopoutPanelManager.Shared;
+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 PopoutSeparationManager
+ public class PanelPopOutManager
{
private const int RETRY_COUNT = 5;
- private IntPtr _simulatorHandle;
- private UserProfileData _profile;
- private List _panels;
- public PopoutSeparationManager(IntPtr simulatorHandle, UserProfileData profile)
+ private UserProfileManager _userProfileManager;
+ private SimConnectManager _simConnectManager;
+ private IntPtr _simulatorHandle;
+ private List _panels;
+ private int _currentPanelIndex;
+
+ public event EventHandler OnPopOutStarted;
+ public event EventHandler> OnPopOutCompleted;
+
+ public UserProfile UserProfile { get; set; }
+
+ public AppSetting AppSetting { get; set; }
+
+ public PanelPopOutManager(UserProfileManager userProfileManager, SimConnectManager simConnectManager)
{
- _simulatorHandle = simulatorHandle;
- _profile = profile;
- _panels = new List();
+ _userProfileManager = userProfileManager;
+ _simConnectManager = simConnectManager;
}
- public bool StartPopout()
+ public void StartPopout()
{
+ var simulatorProcess = DiagnosticManager.GetSimulatorProcess();
+
+ if(simulatorProcess != null)
+ _simulatorHandle = simulatorProcess.Handle;
+
+ _panels = new List();
+
+ OnPopOutStarted?.Invoke(this, null);
+
// If enable, load the current viewport into custom view by Ctrl-Alt-0
- if (FileManager.ReadAppSettingData().UseAutoPanning)
+ if (AppSetting.UseAutoPanning)
{
- var simualatorProcess = WindowManager.GetSimulatorProcess();
+ var simualatorProcess = DiagnosticManager.GetSimulatorProcess();
if (simualatorProcess != null)
{
InputEmulationManager.LoadCustomViewZero(simualatorProcess.Handle);
@@ -41,54 +62,62 @@ namespace MSFSPopoutPanelManager.Provider
});
popoutPanelTask.Wait();
- var popoutReslts = popoutPanelTask.Result;
+ var popoutResults = popoutPanelTask.Result;
- if (popoutReslts != null)
+ if (popoutResults != null)
{
- if (_profile.PanelConfigs.Count > 0)
+ if (UserProfile.PanelConfigs.Count > 0)
{
- LoadAndApplyPanelConfigs(popoutReslts);
- Logger.Status("Panels have been popped out succesfully. Previously saved panel settings have been applied.", StatusMessageType.Info);
+ LoadAndApplyPanelConfigs(popoutResults);
+ Logger.LogStatus("Panels have been popped out succesfully and saved panel settings have been applied.", StatusMessageType.Info);
}
else
{
- _profile.PanelConfigs = popoutReslts;
- Logger.Status("Panels have been popped out succesfully.", StatusMessageType.Info);
+ UserProfile.PanelConfigs = new ObservableCollection(popoutResults);
+ Logger.LogStatus("Panels have been popped out succesfully.", StatusMessageType.Info);
}
- // Recenter the view port by Ctrl-Space
- var simualatorProcess = WindowManager.GetSimulatorProcess();
- if (simualatorProcess != null)
+ // Recenter the view port by Ctrl-Space, needs to click on game window
+ var simualatorProcess = DiagnosticManager.GetSimulatorProcess();
+ if (simualatorProcess != null && UserProfile.PanelSourceCoordinates.Count > 0)
{
- InputEmulationManager.CenterView(simualatorProcess.Handle);
+ InputEmulationManager.CenterView(simualatorProcess.Handle, UserProfile.PanelSourceCoordinates[0].X, UserProfile.PanelSourceCoordinates[0].Y);
}
-
- return true;
- }
- return false;
+ _userProfileManager.WriteUserProfiles();
+
+ OnPopOutCompleted?.Invoke(this, new EventArgs(true));
+ }
+ else
+ {
+ OnPopOutCompleted?.Invoke(this, new EventArgs(false));
+ }
}
public List 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)
+ if (_panels.Count > 0)
{
- Logger.BackgroundStatus("Please close all existing panel pop outs before continuing.", StatusMessageType.Error);
+ Logger.LogStatus("Please close all existing panel pop outs before continuing.", StatusMessageType.Error);
return null;
}
_panels.Clear();
- PInvoke.SetForegroundWindow(_simulatorHandle);
+
+ if(_simulatorHandle != IntPtr.Zero)
+ PInvoke.SetForegroundWindow(_simulatorHandle);
try
{
- for (var i = 0; i < _profile.PanelSourceCoordinates.Count; i++)
+ for (var i = 0; i < UserProfile.PanelSourceCoordinates.Count; i++)
{
- PopoutPanel(_profile.PanelSourceCoordinates[i].X, _profile.PanelSourceCoordinates[i].Y);
+ PopoutPanel(UserProfile.PanelSourceCoordinates[i].X, UserProfile.PanelSourceCoordinates[i].Y);
if (i == 0)
{
@@ -109,14 +138,14 @@ namespace MSFSPopoutPanelManager.Provider
}
if (GetPopoutPanelCountByType(PanelType.CustomPopout) != i + 1)
- throw new PopoutManagerException("Unable to pop out the first panel. Please align first panel's number circle and check if the first panel has already been popped out. Also please check for window obstruction. Process stopped.");
+ 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.");
}
if (i >= 1) // only separate with 2 or more panels
{
int retry = 0;
while (retry < RETRY_COUNT)
{
- SeparatePanel(i, _panels[0].PanelHandle);
+ SeparatePanel(i, _panels[0].PanelHandle); // The joined panel is always the first panel that got popped out
PInvoke.EnumWindows(new PInvoke.CallBack(EnumCustomPopoutCallBack), i);
if (GetPopoutPanelCountByType(PanelType.CustomPopout) != i + 1)
@@ -134,42 +163,46 @@ namespace MSFSPopoutPanelManager.Provider
}
if (GetPopoutPanelCountByType(PanelType.CustomPopout) != i + 1)
- throw new PopoutManagerException($"Unable to pop out panel number {i + 1}. Please align the panel's number circle and check if the panel has already been popped out. Also please check for window obstruction.");
+ throw new PopoutManagerException($"Unable to pop out panel number {i + 1}. 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.");
}
}
// Performance validation, make sure the number of pop out panels is equal to the number of selected panel
- if (GetPopoutPanelCountByType(PanelType.CustomPopout) != _profile.PanelSourceCoordinates.Count)
- throw new PopoutManagerException("Unable to pop out all panels. Please align all panel number circles with in-game panel locations. Also please check for window obstruction ");
+ 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
- PInvoke.EnumWindows(new PInvoke.CallBack(EnumBuiltinPopoutCallBack), _profile.PanelSourceCoordinates.Count + 1);
+ 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), _profile.PanelSourceCoordinates.Count + 1);
+ PInvoke.EnumWindows(new PInvoke.CallBack(EnumMSFSTouchPanelPopoutCallBack), 0);
if (_panels.Count == 0)
- throw new PopoutManagerException("No panels have been found. Please select or open at least one in-game panel or MSFS Touch Panel App's panel.");
+ 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--)
{
- var shift = _panels.Count - i - 1;
- _panels[i].Top = shift * 30;
- _panels[i].Left = shift * 30;
- _panels[i].Width = 800;
- _panels[i].Height = 600;
+ 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;
- PInvoke.MoveWindow(_panels[i].PanelHandle, _panels[i].Top, _panels[i].Left, _panels[i].Width, _panels[i].Height, true);
- PInvoke.SetForegroundWindow(_panels[i].PanelHandle);
- Thread.Sleep(200);
+ PInvoke.MoveWindow(_panels[i].PanelHandle, _panels[i].Top, _panels[i].Left, _panels[i].Width, _panels[i].Height, true);
+ PInvoke.SetForegroundWindow(_panels[i].PanelHandle);
+ Thread.Sleep(200);
+ }
}
return _panels;
}
- catch(PopoutManagerException ex)
+ catch (PopoutManagerException ex)
{
- Logger.BackgroundStatus(ex.Message, StatusMessageType.Error);
+ Logger.LogStatus(ex.Message, StatusMessageType.Error);
return null;
}
catch
@@ -185,28 +218,33 @@ namespace MSFSPopoutPanelManager.Provider
{
if (resultPanel.PanelType == PanelType.CustomPopout)
{
- index = _profile.PanelConfigs.FindIndex(x => x.PanelIndex == resultPanel.PanelIndex);
+ index = UserProfile.PanelConfigs.ToList().FindIndex(x => x.PanelIndex == resultPanel.PanelIndex);
if (index > -1)
- _profile.PanelConfigs[index].PanelHandle = resultPanel.PanelHandle;
+ UserProfile.PanelConfigs[index].PanelHandle = resultPanel.PanelHandle;
}
else
{
- index = _profile.PanelConfigs.FindIndex(x => x.PanelName == resultPanel.PanelName);
+ index = UserProfile.PanelConfigs.ToList().FindIndex(x => x.PanelName == resultPanel.PanelName);
if (index > -1)
- _profile.PanelConfigs[index].PanelHandle = resultPanel.PanelHandle;
+ UserProfile.PanelConfigs[index].PanelHandle = resultPanel.PanelHandle;
else
- _profile.PanelConfigs.Add(resultPanel);
+ UserProfile.PanelConfigs.Add(resultPanel);
}
});
- //_profile.PanelConfigs.RemoveAll(x => x.PanelHandle == IntPtr.Zero && x.PanelType == PanelType.BuiltInPopout);
- _profile.PanelConfigs.RemoveAll(x => x.PanelHandle == IntPtr.Zero);
-
- //_profile.PanelSettings.ForEach(panel =>
- Parallel.ForEach(_profile.PanelConfigs, panel =>
+ // Remove pop out that do not exist for this pop out iteration
+ foreach(var panelConfig in UserProfile.PanelConfigs.ToList())
{
- if (panel != null && panel.Width != 0 && panel.Height != 0)
+ 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)
@@ -220,7 +258,9 @@ namespace MSFSPopoutPanelManager.Provider
}
// Apply locations
- PInvoke.MoveWindow(panel.PanelHandle, panel.Left, panel.Top, panel.Width, panel.Height, true);
+ PInvoke.ShowWindow(panel.PanelHandle, PInvokeConstant.SW_RESTORE);
+ Thread.Sleep(250);
+ PInvoke.MoveWindow(panel.PanelHandle, panel.Left, panel.Top, panel.Width, panel.Height, false);
Thread.Sleep(1000);
// Apply always on top
@@ -264,11 +304,14 @@ namespace MSFSPopoutPanelManager.Provider
// MSFS draws popout panel differently at different time for same panel
PInvoke.MoveWindow(hwnd, -8, 0, 800, 600, true);
PInvoke.SetForegroundWindow(hwnd);
- Thread.Sleep(250);
+ Thread.Sleep(500);
// Find the magnifying glass coordinate
var point = AnalyzeMergedWindows(hwnd);
+ if (point.Y <= 39) // false positive
+ return;
+
InputEmulationManager.LeftClick(point.X, point.Y);
}
@@ -276,11 +319,12 @@ namespace MSFSPopoutPanelManager.Provider
{
var panelInfo = GetPanelWindowInfo(hwnd);
- if(panelInfo != null && panelInfo.PanelType == PanelType.CustomPopout)
+ if (panelInfo != null && panelInfo.PanelType == PanelType.CustomPopout)
{
if (!_panels.Exists(x => x.PanelHandle == hwnd))
{
- panelInfo.PanelIndex = index + 1; // Panel index starts at 1
+ Interlocked.Increment(ref _currentPanelIndex);
+ panelInfo.PanelIndex = _currentPanelIndex; // PanelIndex starts at 1
_panels.Add(panelInfo);
}
}
@@ -296,7 +340,8 @@ namespace MSFSPopoutPanelManager.Provider
{
if (!_panels.Exists(x => x.PanelHandle == hwnd))
{
- panelInfo.PanelIndex = index;
+ Interlocked.Increment(ref _currentPanelIndex);
+ panelInfo.PanelIndex = _currentPanelIndex;
_panels.Add(panelInfo);
}
}
@@ -312,7 +357,8 @@ namespace MSFSPopoutPanelManager.Provider
{
if (!_panels.Exists(x => x.PanelHandle == hwnd))
{
- panelInfo.PanelIndex = index;
+ Interlocked.Increment(ref _currentPanelIndex);
+ panelInfo.PanelIndex = _currentPanelIndex;
_panels.Add(panelInfo);
}
}
@@ -369,7 +415,7 @@ namespace MSFSPopoutPanelManager.Provider
PInvoke.GetClientRect(hwnd, out rectangle);
var panelMenubarTop = GetPanelMenubarTop(sourceImage, rectangle);
- if (panelMenubarTop > sourceImage.Height)
+ if (panelMenubarTop > sourceImage.Height)
return Point.Empty;
var panelMenubarBottom = GetPanelMenubarBottom(sourceImage, rectangle);
@@ -463,7 +509,7 @@ namespace MSFSPopoutPanelManager.Provider
// found the top of menu bar
menubarBottom = y + top;
}
- else if(menubarBottom > -1) /// it is no longer white in color, we hit menubar bottom
+ else if (menubarBottom > -1) /// it is no longer white in color, we hit menubar bottom
{
sourceImage.UnlockBits(stripData);
return menubarBottom;
diff --git a/Provider/PanelSelectionManager.cs b/Provider/PanelSelectionManager.cs
index c23a9a4..6abf28d 100644
--- a/Provider/PanelSelectionManager.cs
+++ b/Provider/PanelSelectionManager.cs
@@ -1,108 +1,159 @@
-using Gma.System.MouseKeyHook;
+using MSFSPopoutPanelManager.Model;
using MSFSPopoutPanelManager.Shared;
-using MSFSPopoutPanelManager.UI;
using System;
using System.Collections.Generic;
-using System.Threading;
-using System.Windows.Forms;
+using System.Drawing;
+using System.Linq;
namespace MSFSPopoutPanelManager.Provider
{
public class PanelSelectionManager
{
- private IKeyboardMouseEvents _mouseHook;
+ private UserProfileManager _userProfileManager;
private int _panelIndex;
- private Form _appForm;
+ private List _panelCoordinates;
+ private IntPtr _winEventHook;
+ private static PInvoke.WinEventProc _winEvent; // keep this as static to prevent garbage collect or the app will crash
+ private Rectangle _lastWindowRectangle;
+ private bool _isEditingPanelCoordinates;
- public event EventHandler OnSelectionCompleted;
+ public event EventHandler OnPanelSelectionCompleted;
+ public event EventHandler> OnPanelLocationAdded;
+ public event EventHandler OnPanelLocationRemoved;
+ public event EventHandler OnAllPanelLocationsRemoved;
- public List PanelCoordinates { get; set; }
+ public UserProfile UserProfile { get; set; }
- public PanelSelectionManager(Form form)
+ public AppSetting AppSetting { get; set; }
+
+ public PanelSelectionManager(UserProfileManager userProfileManager)
{
- PanelCoordinates = new List();
- _appForm = form;
+ _userProfileManager = userProfileManager;
+
+ InputHookManager.OnPanelSelectionAdded += HandleOnPanelSelectionAdded;
+ InputHookManager.OnPanelSelectionRemoved += HandleOnPanelSelectionRemoved;
+ InputHookManager.OnPanelSelectionCompleted += HandleOnPanelSelectionCompleted;
}
public void Start()
{
- if (_mouseHook == null)
- {
- _mouseHook = Hook.GlobalEvents();
- _mouseHook.MouseDownExt += HandleMouseHookMouseDownExt;
- }
-
_panelIndex = 1;
- ShowPanelLocationOverlay(true);
+ _panelCoordinates = new List();
+ ShowPanelLocationOverlay(_panelCoordinates, true);
+ InputHookManager.SubscribeToPanelSelectionEvent = true;
}
- public void Reset()
- {
- _panelIndex = 1;
- ShowPanelLocationOverlay(false);
- }
-
- public void ShowPanelLocationOverlay(bool show)
+ public void ShowPanelLocationOverlay(List panelCoordinates, bool show)
{
// close all overlays
- for (int i = Application.OpenForms.Count - 1; i >= 0; i--)
- {
- if (Application.OpenForms[i].GetType() == typeof(PopoutCoorOverlayForm))
- Application.OpenForms[i].Close();
- }
+ OnAllPanelLocationsRemoved?.Invoke(this, null);
- if (show && PanelCoordinates.Count > 0)
+ if (show && panelCoordinates.Count > 0)
{
- foreach (var coor in PanelCoordinates)
- WindowManager.AddPanelLocationSelectionOverlay(coor.PanelIndex.ToString(), coor.X, coor.Y);
+ foreach (var coor in panelCoordinates)
+ {
+ var panelSourceCoordinate = new PanelSourceCoordinate() { PanelIndex = coor.PanelIndex, X = coor.X, Y = coor.Y };
+ OnPanelLocationAdded?.Invoke(this, new EventArgs(panelSourceCoordinate));
+ }
}
}
- private void HandleMouseHookMouseDownExt(object sender, MouseEventExtArgs e)
+ private void HandleOnPanelSelectionAdded(object sender, System.Drawing.Point e)
{
- if (e.Button == MouseButtons.Left)
+ var newPanelCoordinates = new PanelSourceCoordinate() { PanelIndex = _panelIndex, X = e.X, Y = e.Y };
+ _panelCoordinates.Add(newPanelCoordinates);
+ _panelIndex++;
+
+ OnPanelLocationAdded?.Invoke(this, new EventArgs(newPanelCoordinates));
+ }
+
+ private void HandleOnPanelSelectionRemoved(object sender, EventArgs e)
+ {
+ if(_panelCoordinates.Count > 0)
{
- var ctrlPressed = Control.ModifierKeys.ToString() == "Control";
- var shiftPressed = Control.ModifierKeys.ToString() == "Shift";
+ _panelCoordinates.RemoveAt(_panelCoordinates.Count - 1);
+ _panelIndex--;
- if (ctrlPressed)
+ OnPanelLocationRemoved?.Invoke(this, null);
+ }
+ }
+
+ private void HandleOnPanelSelectionCompleted(object sender, EventArgs e)
+ {
+ // If enable, save the current viewport into custom view by Ctrl-Alt-0
+ if (AppSetting.UseAutoPanning)
+ {
+ var simualatorProcess = DiagnosticManager.GetSimulatorProcess();
+ if (simualatorProcess != null)
{
- if (_mouseHook != null)
- {
- _mouseHook.MouseDownExt -= HandleMouseHookMouseDownExt;
- _mouseHook.Dispose();
- _mouseHook = null;
- }
-
- OnSelectionCompleted?.Invoke(this, null);
+ InputEmulationManager.SaveCustomViewZero(simualatorProcess.Handle);
}
- else if (shiftPressed && Application.OpenForms.Count >= 1)
- {
- if (Application.OpenForms[Application.OpenForms.Count - 1].GetType() == typeof(PopoutCoorOverlayForm))
- {
- // Remove last drawn overlay
- Application.OpenForms[Application.OpenForms.Count - 1].Close();
- PanelCoordinates.RemoveAt(PanelCoordinates.Count - 1);
- _panelIndex--;
- }
- }
- 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)
- {
- var newPanelCoordinates = new PanelSourceCoordinate() { PanelIndex = _panelIndex, X = e.X, Y = e.Y };
- PanelCoordinates.Add(newPanelCoordinates);
+ // Assign and save panel coordinates to active profile
+ UserProfile.PanelSourceCoordinates.Clear();
+ _panelCoordinates.ForEach(c => UserProfile.PanelSourceCoordinates.Add(c));
+ UserProfile.PanelConfigs.Clear();
+ UserProfile.IsLocked = false;
- WindowManager.AddPanelLocationSelectionOverlay(_panelIndex.ToString(), e.X, e.Y);
- _panelIndex++;
- }
+ _userProfileManager.WriteUserProfiles();
+
+ InputHookManager.SubscribeToPanelSelectionEvent = false;
+ OnPanelSelectionCompleted?.Invoke(this, null);
+ }
+
+ public void StartEditPanelLocations()
+ {
+ _winEvent = new PInvoke.WinEventProc(PanelLocationEditEventCallback);
+ _winEventHook = PInvoke.SetWinEventHook(PInvokeConstant.EVENT_SYSTEM_MOVESIZEEND, PInvokeConstant.EVENT_OBJECT_LOCATIONCHANGE, DiagnosticManager.GetApplicationProcess().Handle, _winEvent, 0, 0, PInvokeConstant.WINEVENT_OUTOFCONTEXT);
+ _isEditingPanelCoordinates = true;
+ }
+
+ public void EndEditPanelLocations()
+ {
+ PInvoke.UnhookWinEvent(_winEventHook);
+ _isEditingPanelCoordinates = false;
+ }
+
+ private void PanelLocationEditEventCallback(IntPtr hWinEventHook, uint iEvent, IntPtr hWnd, int idObject, int idChild, int dwEventThread, int dwmsEventTime)
+ {
+ // check by priority to minimize escaping constraint
+ if (hWnd == IntPtr.Zero
+ || idObject != 0
+ || hWinEventHook != _winEventHook
+ || !_isEditingPanelCoordinates
+ || !(iEvent == PInvokeConstant.EVENT_OBJECT_LOCATIONCHANGE || iEvent == PInvokeConstant.EVENT_SYSTEM_MOVESIZEEND)
+ || UserProfile.PanelSourceCoordinates == null || UserProfile.PanelSourceCoordinates.Count == 0)
+ {
+ return;
+ }
+
+ var panelSourceCoordinate = UserProfile.PanelSourceCoordinates.FirstOrDefault(panel => panel.PanelHandle == hWnd);
+
+ if (panelSourceCoordinate != null)
+ {
+ switch (iEvent)
+ {
+ case PInvokeConstant.EVENT_OBJECT_LOCATIONCHANGE:
+ Rectangle winRectangle;
+ PInvoke.GetWindowRect(panelSourceCoordinate.PanelHandle, out winRectangle);
+
+ if (_lastWindowRectangle == winRectangle) // ignore duplicate callback messages
+ return;
+
+ _lastWindowRectangle = winRectangle;
+ Rectangle clientRectangle;
+ PInvoke.GetClientRect(panelSourceCoordinate.PanelHandle, out clientRectangle);
+
+ panelSourceCoordinate.X = winRectangle.Left;
+ panelSourceCoordinate.Y = winRectangle.Top;
+
+ break;
+ case PInvokeConstant.EVENT_SYSTEM_MOVESIZEEND:
+ _userProfileManager.WriteUserProfiles();
+ break;
}
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Provider/Provider.csproj b/Provider/Provider.csproj
new file mode 100644
index 0000000..a214021
--- /dev/null
+++ b/Provider/Provider.csproj
@@ -0,0 +1,47 @@
+
+
+
+ Library
+ net5.0-windows
+ true
+ MSFSPopoutPanelManager.Provider
+ MSFS 2020 Popout Panel Manager Provider
+ MSFS 2020 Popout Panel Manager Provider
+ 3.2.0
+ Stanley Kwok
+ Stanley Kwok 2021
+ https://github.com/hawkeye-stan/msfs-popout-panel-manager
+ true
+
+
+ AnyCPU;x64
+
+
+
+ true
+
+
+
+ true
+
+
+
+ true
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Provider/SimConnectManager.cs b/Provider/SimConnectManager.cs
new file mode 100644
index 0000000..3ae3b6b
--- /dev/null
+++ b/Provider/SimConnectManager.cs
@@ -0,0 +1,131 @@
+using MSFSPopoutPanelManager.FsConnector;
+using MSFSPopoutPanelManager.Shared;
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Threading;
+using System.Timers;
+
+namespace MSFSPopoutPanelManager.Provider
+{
+ public class SimConnectManager
+ {
+ private const int MSFS_DATA_REFRESH_TIMEOUT = 1000;
+
+ private SimConnector _simConnector;
+ private dynamic _simData;
+
+ private System.Timers.Timer _requestDataTimer;
+ private SimConnectSystemEvent _lastSystemEvent;
+ private bool _isPowerOnForPopOut;
+
+ public event EventHandler OnConnected;
+ public event EventHandler OnDisconnected;
+ public event EventHandler> OnSimConnectDataRefreshed;
+ public event EventHandler OnFlightStarted;
+ public event EventHandler OnFlightStopped;
+
+ public bool IsSimConnectStarted { get; set; }
+
+ public SimConnectManager()
+ {
+ _simConnector = new SimConnector();
+ _simConnector.OnConnected += (sender, e) => { OnConnected?.Invoke(this, null); };
+ _simConnector.OnDisconnected += (sender, e) => { OnDisconnected?.Invoke(this, null); };
+ _simConnector.OnReceivedData += HandleDataReceived;
+ _simConnector.OnReceiveSystemEvent += HandleReceiveSystemEvent;
+ _simConnector.OnConnected += (sender, e) =>
+ {
+ _requestDataTimer = new System.Timers.Timer();
+ _requestDataTimer.Interval = MSFS_DATA_REFRESH_TIMEOUT;
+ _requestDataTimer.Enabled = true;
+ _requestDataTimer.Elapsed += HandleDataRequested;
+ _requestDataTimer.Elapsed += HandleMessageReceived;
+ };
+
+ _simConnector.Start();
+ }
+
+ public void Stop()
+ {
+ _simConnector.Stop();
+ }
+
+ public void Restart()
+ {
+ _simConnector.StopAndReconnect();
+ }
+
+ public void TurnOnPower(bool isRequiredForColdStart)
+ {
+ if (isRequiredForColdStart && _simData != null && !_simData.ElectricalMasterBattery)
+ {
+ _isPowerOnForPopOut = true;
+ _simConnector.TransmitActionEvent(ActionEvent.KEY_MASTER_BATTERY_SET, 1);
+ Thread.Sleep(100);
+ _simConnector.TransmitActionEvent(ActionEvent.KEY_ALTERNATOR_SET, 1);
+ Thread.Sleep(100);
+ _simConnector.TransmitActionEvent(ActionEvent.KEY_AVIONICS_MASTER_1_ON, 1);
+ Thread.Sleep(100);
+ _simConnector.TransmitActionEvent(ActionEvent.KEY_AVIONICS_MASTER_2_ON, 1);
+ Thread.Sleep(100);
+ _simConnector.TransmitActionEvent(ActionEvent.KEY_AVIONICS_MASTER_SET, 1);
+ }
+ }
+
+ public void TurnOffpower()
+ {
+ if(_isPowerOnForPopOut)
+ {
+ _simConnector.TransmitActionEvent(ActionEvent.KEY_AVIONICS_MASTER_1_OFF, 1);
+ Thread.Sleep(100);
+ _simConnector.TransmitActionEvent(ActionEvent.KEY_AVIONICS_MASTER_2_OFF, 1);
+ Thread.Sleep(100);
+ _simConnector.TransmitActionEvent(ActionEvent.KEY_AVIONICS_MASTER_SET, 0);
+ Thread.Sleep(100);
+ _simConnector.TransmitActionEvent(ActionEvent.KEY_ALTERNATOR_SET, 0);
+ Thread.Sleep(100);
+ _simConnector.TransmitActionEvent(ActionEvent.KEY_MASTER_BATTERY_SET, 0);
+
+ _isPowerOnForPopOut = false;
+ }
+ }
+
+ private void HandleDataRequested(object sender, ElapsedEventArgs e)
+ {
+ try
+ {
+ _simConnector.RequestData();
+ }
+ catch { }
+ }
+
+ private void HandleMessageReceived(object sender, ElapsedEventArgs e)
+ {
+ try
+ {
+ _simConnector.ReceiveMessage();
+ }
+ catch { }
+ }
+
+ public void HandleDataReceived(object sender, EventArgs e)
+ {
+ _simData = e.Value;
+ OnSimConnectDataRefreshed?.Invoke(this, new EventArgs(e.Value));
+ }
+
+ private void HandleReceiveSystemEvent(object sender, EventArgs e)
+ {
+ // to detect flight start at the "Ready to Fly" screen, it has a SIMSTART follows by a VIEW event
+ if(_lastSystemEvent == SimConnectSystemEvent.SIMSTART && e.Value == SimConnectSystemEvent.VIEW)
+ OnFlightStarted?.Invoke(this, null);
+
+ if (e.Value == SimConnectSystemEvent.SIMSTOP)
+ OnFlightStopped?.Invoke(this, null);
+
+ Debug.WriteLine($"SimConnectSystemEvent Received: {e.Value.ToString()}");
+ _lastSystemEvent = e.Value;
+ }
+ }
+}
diff --git a/Provider/UserProfileManager.cs b/Provider/UserProfileManager.cs
new file mode 100644
index 0000000..4b17809
--- /dev/null
+++ b/Provider/UserProfileManager.cs
@@ -0,0 +1,168 @@
+using MSFSPopoutPanelManager.Model;
+using MSFSPopoutPanelManager.Shared;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+
+namespace MSFSPopoutPanelManager.Provider
+{
+ public class UserProfileManager
+ {
+ private const string USER_PROFILE_DATA_FILENAME = "userprofiledata.json";
+
+ public ObservableCollection UserProfiles { get; set; }
+
+ public int AddUserProfile(string newProfileName)
+ {
+ return AddProfile(new UserProfile(), newProfileName);
+ }
+
+ public int AddUserProfileByCopyingProfile(string newProfileName, int copyProfileId)
+ {
+ if (UserProfiles == null)
+ throw new Exception("User Profiles is null.");
+
+ var matchedProfile = UserProfiles.FirstOrDefault(p => p.ProfileId == copyProfileId);
+
+ var copiedProfile = matchedProfile.Copy(); // Using Shared/ObjectExtensions.cs extension method
+ copiedProfile.IsDefaultProfile = false;
+ copiedProfile.BindingPlaneTitle = null;
+
+ return AddProfile(copiedProfile, newProfileName);
+ }
+
+ public bool DeleteUserProfile(int profileId)
+ {
+ if (UserProfiles == null)
+ throw new Exception("User Profiles is null.");
+
+ if (profileId == -1)
+ return false;
+
+ var profileToRemove = UserProfiles.First(x => x.ProfileId == profileId);
+ UserProfiles.Remove(profileToRemove);
+ WriteUserProfiles();
+
+ Logger.LogStatus($"Profile '{profileToRemove.ProfileName}' has been deleted successfully.", StatusMessageType.Info);
+
+ return true;
+ }
+
+ public bool SetDefaultUserProfile(int profileId)
+ {
+ if (UserProfiles == null)
+ throw new Exception("User Profiles is null.");
+
+ if (profileId == -1)
+ return false;
+
+ var profile = UserProfiles.First(x => x.ProfileId == profileId);
+
+ profile.IsDefaultProfile = true;
+ foreach (var p in UserProfiles)
+ {
+ if (p.ProfileId != profileId)
+ p.IsDefaultProfile = false;
+ }
+
+ WriteUserProfiles();
+
+ Logger.LogStatus($"Profile '{profile.ProfileName}' has been set as default.", StatusMessageType.Info);
+
+ return true;
+ }
+
+ public UserProfile GetDefaultProfile()
+ {
+ return UserProfiles.ToList().Find(x => x.IsDefaultProfile);
+ }
+
+ public void AddProfileBinding(string planeTitle, int activeProfileId)
+ {
+ var bindedProfile = UserProfiles.FirstOrDefault(p => p.BindingPlaneTitle == planeTitle);
+ if (bindedProfile != null)
+ {
+ Logger.LogStatus($"Unable to add binding to the profile because '{planeTitle}' was already bound to profile '{bindedProfile.ProfileName}'.", StatusMessageType.Error);
+ return;
+ }
+
+ UserProfiles.First(p => p.ProfileId == activeProfileId).BindingPlaneTitle = planeTitle;
+ WriteUserProfiles();
+ Logger.LogStatus($"Binding for the profile has been added successfully.", StatusMessageType.Info);
+ }
+
+ public void DeleteProfileBinding(int activeProfileId)
+ {
+ UserProfiles.First(p => p.ProfileId == activeProfileId).BindingPlaneTitle = null;
+ WriteUserProfiles();
+ Logger.LogStatus($"Binding for the profile has been deleted successfully.", StatusMessageType.Info);
+ }
+
+ public void ReadUserProfiles()
+ {
+ try
+ {
+ using (StreamReader reader = new StreamReader(Path.Combine(FileIo.GetUserDataFilePath(), USER_PROFILE_DATA_FILENAME)))
+ {
+ UserProfiles = new ObservableCollection(JsonConvert.DeserializeObject>(reader.ReadToEnd()));
+ }
+ }
+ catch
+ {
+ UserProfiles = new ObservableCollection(new List());
+ }
+ }
+
+ public void WriteUserProfiles()
+ {
+ Debug.WriteLine("saving profile....");
+
+ if (UserProfiles == null)
+ throw new Exception("User Profiles is null.");
+
+ try
+ {
+ var userProfilePath = FileIo.GetUserDataFilePath();
+
+ if (!Directory.Exists(userProfilePath))
+ Directory.CreateDirectory(userProfilePath);
+
+ using (StreamWriter file = File.CreateText(Path.Combine(userProfilePath, USER_PROFILE_DATA_FILENAME)))
+ {
+ JsonSerializer serializer = new JsonSerializer();
+ serializer.Serialize(file, UserProfiles);
+ }
+ }
+ catch
+ {
+ Logger.LogStatus($"Unable to write user data file: {USER_PROFILE_DATA_FILENAME}", StatusMessageType.Error);
+ }
+ }
+
+ private int AddProfile(UserProfile userProfile, string newProfileName)
+ {
+ if (UserProfiles == null)
+ throw new Exception("User Profiles is null.");
+
+ var newPlaneProfile = userProfile;
+ var newProfileId = UserProfiles.Count > 0 ? UserProfiles.Max(x => x.ProfileId) + 1 : 1;
+
+ newPlaneProfile.ProfileName = newProfileName;
+ newPlaneProfile.ProfileId = newProfileId;
+
+ var tmpList = UserProfiles.ToList();
+ tmpList.Add(newPlaneProfile);
+ var index = tmpList.OrderBy(x => x.ProfileName).ToList().FindIndex(x => x.ProfileId == newProfileId);
+ UserProfiles.Insert(index, newPlaneProfile);
+ WriteUserProfiles();
+
+ Logger.LogStatus($"Profile '{newPlaneProfile.ProfileName}' has been added successfully.", StatusMessageType.Info);
+
+ return newProfileId;
+ }
+ }
+}
diff --git a/Provider/WindowManager.cs b/Provider/WindowManager.cs
index 45f5860..5ff3b3e 100644
--- a/Provider/WindowManager.cs
+++ b/Provider/WindowManager.cs
@@ -1,23 +1,12 @@
using MSFSPopoutPanelManager.Shared;
-using MSFSPopoutPanelManager.UI;
using System;
-using System.Diagnostics;
using System.Drawing;
-using System.Windows.Forms;
+using System.Runtime.InteropServices;
namespace MSFSPopoutPanelManager.Provider
{
public class WindowManager
{
- 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();
- }
-
public static void ApplyHidePanelTitleBar(IntPtr handle, bool hideTitleBar)
{
var currentStyle = PInvoke.GetWindowLong(handle, PInvokeConstant.GWL_STYLE).ToInt64();
@@ -31,9 +20,20 @@ namespace MSFSPopoutPanelManager.Provider
public static void ApplyAlwaysOnTop(IntPtr handle, bool alwaysOnTop, Rectangle panelRectangle)
{
if (alwaysOnTop)
- PInvoke.SetWindowPos(handle, PInvokeConstant.HWND_TOPMOST, panelRectangle.Left, panelRectangle.Top, panelRectangle.Width, panelRectangle.Height, PInvokeConstant.SWP_ALWAYS_ON_TOP);
+ PInvoke.SetWindowPos(handle, new IntPtr(PInvokeConstant.HWND_TOPMOST), panelRectangle.Left, panelRectangle.Top, panelRectangle.Width, panelRectangle.Height, PInvokeConstant.SWP_ALWAYS_ON_TOP);
else
- PInvoke.SetWindowPos(handle, PInvokeConstant.HWND_NOTOPMOST, panelRectangle.Left, panelRectangle.Top, panelRectangle.Width, panelRectangle.Height, 0);
+ PInvoke.SetWindowPos(handle, new IntPtr(PInvokeConstant.HWND_NOTOPMOST), panelRectangle.Left, panelRectangle.Top, panelRectangle.Width, panelRectangle.Height, 0);
+ }
+
+ public static void ApplyAlwaysOnTop(IntPtr handle, bool alwaysOnTop)
+ {
+ Rectangle rect;
+ PInvoke.GetWindowRect(handle, out rect);
+
+ Rectangle clientRectangle;
+ PInvoke.GetClientRect(handle, out clientRectangle);
+
+ ApplyAlwaysOnTop(handle, alwaysOnTop, new Rectangle(rect.X, rect.Y, clientRectangle.Width, clientRectangle.Height));
}
public static void CloseWindow(IntPtr handle)
@@ -48,32 +48,15 @@ namespace MSFSPopoutPanelManager.Provider
PInvoke.MoveWindow(handle, x, y, rectangle.Width, rectangle.Height, false);
}
- public static WindowProcess GetSimulatorProcess()
+ public static void MinimizeWindow(IntPtr handle)
{
- return GetProcess("FlightSimulator");
+ PInvoke.ShowWindow(handle, PInvokeConstant.SW_MINIMIZE);
}
- public static WindowProcess GetApplicationProcess()
+ public static void BringWindowToForeground(IntPtr handle)
{
- return GetProcess("MSFSPopoutPanelManager");
- }
-
- private static WindowProcess GetProcess(string processName)
- {
- foreach (var process in Process.GetProcesses())
- {
- if (process.ProcessName == processName)
- {
- return new WindowProcess()
- {
- ProcessId = process.Id,
- ProcessName = process.ProcessName,
- Handle = process.MainWindowHandle
- };
- }
- }
-
- return null;
+ PInvoke.ShowWindowAsync(new HandleRef(null, handle), PInvokeConstant.SW_RESTORE);
+ PInvoke.SetForegroundWindow(handle);
}
public static void CloseAllCustomPopoutPanels()
@@ -81,6 +64,34 @@ namespace MSFSPopoutPanelManager.Provider
PInvoke.EnumWindows(new PInvoke.CallBack(EnumAllCustomPopoutPanels), 1);
}
+ public static void MinimizeAllPopoutPanels(bool active)
+ {
+ if (active)
+ {
+ PInvoke.EnumWindows(new PInvoke.CallBack(EnumToMinimizePopoutPanels), 0);
+ }
+ else
+ {
+ PInvoke.EnumWindows(new PInvoke.CallBack(EnumToMinimizePopoutPanels), 1);
+ }
+ }
+
+ private static bool EnumToMinimizePopoutPanels(IntPtr hwnd, int index)
+ {
+ var className = PInvoke.GetClassName(hwnd);
+ var caption = PInvoke.GetWindowText(hwnd);
+
+ if (className == "AceApp" && caption.IndexOf("Microsoft Flight Simulator") == -1) // MSFS windows designation
+ {
+ if (index == 0)
+ PInvoke.ShowWindow(hwnd, PInvokeConstant.SW_MINIMIZE);
+ else
+ PInvoke.ShowWindow(hwnd, PInvokeConstant.SW_RESTORE);
+ }
+
+ return true;
+ }
+
private static bool EnumAllCustomPopoutPanels(IntPtr hwnd, int index)
{
var className = PInvoke.GetClassName(hwnd);
@@ -90,7 +101,7 @@ namespace MSFSPopoutPanelManager.Provider
{
WindowManager.CloseWindow(hwnd);
}
- else if (className == "AceApp") // for builtin pop out (ATC, VFR Map, ect)
+ else if (className == "AceApp" && caption.IndexOf("Microsoft Flight Simulator") == -1) // for builtin pop out (ATC, VFR Map, ect)
{
WindowManager.MoveWindow(hwnd, 0, 0);
}
diff --git a/Shared/WindowProcess.cs b/Provider/WindowProcess.cs
similarity index 82%
rename from Shared/WindowProcess.cs
rename to Provider/WindowProcess.cs
index 9817b2a..a0308a9 100644
--- a/Shared/WindowProcess.cs
+++ b/Provider/WindowProcess.cs
@@ -1,6 +1,6 @@
using System;
-namespace MSFSPopoutPanelManager
+namespace MSFSPopoutPanelManager.Provider
{
public class WindowProcess
{
diff --git a/README.md b/README.md
index 99b3ad2..219761b 100644
--- a/README.md
+++ b/README.md
@@ -3,18 +3,55 @@ MSFS Pop Out Panel Manager is an application for MSFS 2020 which helps pop out,
[FlightSimulator.com forum thread regarding this project](https://forums.flightsimulator.com/t/msfs-pop-out-panel-manager-automatically-pop-out-and-save-panel-position/460613)
-### IMPORTANT! Version 3.0 file format is not compatible with previous version user profile data format since additional information has been added and updated to support new features. Please continue to use version 2.2 of the application if it works for you and you do not need the new features. If you're technical, please see User Profile Data Files below to do a manual data transfer.
+## Version 3.2 NEW FEATURES!
+* Added per monitor DPI-awareness support. The application should run and display correctly when using combination of mixed monitor (with high-DPI and low-DPI) resolutions and scaling.
+* Added system tray icon access. Application can start minimize or minimize to system tray. System tray icon features a context menu to allow quick access to application functions.
+* Added user requested feature to provide keyboard shortcut (Ctrl-Alt-P) to start panel pop out with either an active profile or a default profile selected.
+* New copy profile feature. You can reuse your defined panel settings for another plane or plane/livery combination. This is a feature to solve the problem when the final panel placements are the same but the in-game panel locations are different. This also allows using a defined profile for different liveries for the same plane for Auto Pop Out. (See Auto Pop Out Panel experiment feature).
+* Added quick panel location selection adjustment feature. You can now adjust panel locations without redoing the entire profile. Just click "Show/Edit Panel Location Overlay" checkbox, you can now drag and move the panel selection number circle. When you're done with the placement, the new location will automatically save for the profile.
+* Added Save Auto Panning Camera Angle function if you need to adjust the in-game camera angle during panel selection.
+* New logo icon for the app.
+* New dark theme for the entire UI.
+* Technical Note - Application is ported and rewritten with .NET WPF framework instead of WinForms and SimConnect is added to the app for Auto Pop Out feature and for future functionality expansions.
-## Version 3.0 NEW FEATURES!
+** Beta feature - Auto Pop Out Panels**
-* Provided 2X pop out and panel separation performance.
-* Display resolution independent. Tested on 1080p/1440p/4k display.
-* New Cold Start feature. Panels can be popped out and recalled later even when they're not turned on.
-* New Auto Panning feature remembers the cockpit camera angle when you first define the pop out panels. You can now pan, zoom in, and zoom out to identify offscreen panels and the camera angle will be saved and reused. This feature requires the use of Ctrl-Alt-0 and Alt-0 keyboard binding to save custom camera view per plane configuration. If the keyboard binding is currently being used. The auto-panning feature will overwrite the saved camera view if enabled.
-* New fine-grain control in positioning panels down to pixel level.
-* New user-friendly features such as Always on Top, real time readout as you position panels, a more intuitive user interface and status messages.
-* New auto save feature. All profile and panel changes get save automatically.
-* Technical: Rewritten code base to improve code structure and performance.
+When a profile is defined, final panel placements are set, and bound to a plane type + livery combination. The application will automatically pop out all panels if a matched profile is detected when a flight starts.
+
+[Online Video - feature in action](https://vimeo.com/674073559) - In the video, after clicking flight restart, the app did all the clicking by itself.
+
+**How it works and how to use:**
+
+The app will try to find a matching profile with the title of the plane (per livery). It will then automatically detect when a flight is starting and then click the "Ready to Fly" button. It will then power on instrumentation for cold start (if necessary), and pop out all panels. This feature allows panels to be popped out without the need of user interaction. If profiles are set and bound, you can auto-start the app minimized in system and as you start your flight, panels will automatically pop out for you.
+
+* First make sure in File->Preferences, "Auto Pop Out Panels" option is turned on.
+
+* For existing profile to use Auto Pop Out feature, just click "Add Binding" and bind the profile to the active plane in the game.
+
+* Since Auto Pop Out need to match plane title to work, a profile must be bound to a plane to use the Auto Pop Out feature. You can continue to manually click start the pop out for unbound profile. Or better yet, use the new keyboard shortcut (Ctrl-Alt-P) to manually start the pop out process.
+
+* During my testing, instrumentations only need to be powered on for Auto Pop Out to work for G1000/G1000 NXi plane during cold start. (There seems to be a bug in the game that you can't do Alt-Right click to pop out cold start panel for this particular plane variant). For other plane instrumentations I've tested (G3000, CJ4, Aerosoft CRJ 550/700, FBW A32NX), panels can be popped out without powering on. So please make sure the checkbox "Power on required to pop out panels on cold start" is checked for G1000 related profiles.
+
+* If you want to fly the same plane with different livery, the plane title will be different for each livery. You can see the current in-game active plane + livery title by hovering your mouse over the connection icon in the upper right corner of the app. In order to use Auto Pop Out feature in this scenario, first defined the initial profile for a plane + livery combination. After you're satisfy the profile is working, switch the plane livery and create a new profile by copying the initial profile and bound it to the plane again (with the new livery selected). You should notice the binding name will be different even though all the panel settings are the same. Repeat this for as many liveries for the plane as needed.
+
+* If after binding the livery and Auto Pop Out did not work (which means the app cannot find a match), please hover your mouse over the connecting icon in the app to see the current plane title reported by the game is the same as what you've bound to the profile. Sometimes, SimConnect does not update the plane title until the flight actually starts. So if you've done the binding before the flight start, sometime it will not work. Just just "Replace binding" again and confirm the plane title.
+
+* **TIPS:** One of the trick to force SimConnect to update the plane title after selecting a new livery is when you've selected a plane and livery in the World Map, click the "Go Back" button at the lower left of your screen.
+
+* **TIPS:** For technical user, there are parts in this feature that uses a timer when waiting to execute certain steps in the Auto Pop Out process. An example is how long to wait for the "Ready to Fly" button to appear because auto-clicking it. Depending on the speed of your machine, you can adjust the wait timer to speed up or slow down the auto pop out process. Please see the section "User Profile Data Files" in the documentation for instruction to edit appSettings.json file.
+
+
+
+## Application Features
+
+* Display resolution independent. Supports 1080p/1440p/4k display.
+* Support multiple user defined profiles to save panel locations to be recalled later.
+* Intuitive user interface to defined location of panels to be popped out.
+* Cold Start feature. Panels can be popped out and recalled later even when they're not powered on.
+* Auto Panning feature remembers the cockpit camera angle when you first define the pop out panels. You can now pan, zoom in, and zoom out to identify offscreen panels and the camera angle will be saved and reused. This feature requires the use of Ctrl-Alt-0 and Alt-0 keyboard binding to save custom camera view per plane configuration. If the keyboard binding is currently being used. The auto-panning feature will overwrite the saved camera view if enabled.
+* Fine-grain control in positioning panels down to pixel level.
+* User-friendly features such as application Always on Top and Auto Start as MSFS starts.
+* Auto save feature. All profile and panel changes get save automatically.
## History: Pop Out Panel Positioning Annoyance
@@ -31,45 +68,47 @@ With v3.0, redesign from the ground up about how to pop out and separate the pan
## How to Use?
[Here](images/doc/userguide.mp4) or [Online](https://vimeo.com/668430955) is a video of how the app works.
- 1. Start the application **MSFSPopoutPanelManager.exe** and it will automatically connect when MSFS starts. You maybe prompt to download .NET framework 5.0. Please see the screenshot below to download and install x64 desktop version of the framework.
+ 1. Start the application **MSFSPopoutPanelManager.exe** and it will automatically connect when MSFS/SimConnect starts. You maybe prompt to download .NET framework 5.0. Please see the screenshot below to download and install x64 desktop version of the framework.
- 2. First create a new plane profile (for example A32NX by FlybyWire)
+ 2. First start the game and start a flight. Then, in the app, create a new profile (for example: Cessna 172 G1000)
-
+
- 3. Once the game has started and you're at the beginning of flight, please click "Start Panel Selection" to define where the pop out panels will be using LEFT CLICK. Use CTRL-LEFT CLICK when done to complete the selection.
+ 3. If you want to associate the profile to the current plane to use the Auto Pop Out feature, click "Add Binding".
+
+
+
+
+
+ 3. Now you're ready to select the panels you want to pop out. Please click "Start Panel Selection" to define where the pop out panels will be using LEFT CLICK. Use CTRL-LEFT CLICK when done to complete the selection. You can also move the number circles at this point to do final adjustment.
-
+
- 4. Now, click "Start Pop Out". At this point, please be patient. The application will start popping out and separating panels one by one and you will see a lot of movement on the screen. If something goes wrong, just follow the instruction in the status message and try again.
+ 4. Now, click "Start Pop Out". At this point, please be patient. The application will start popping out and separating panels one by one and you will see a lot of movements on screen. If something goes wrong, just follow the instruction in the status message and try again.
5. Once the process is done, you will see a list of panels line up in the upper left corner of the screen. All the panels are given a default name. You can name them anything you want if desire.
-
+
- 6. Now, start the panel configuration by dragging the pop out panels into their final position. You can also type value directly into the data grid to move and resize a panel. The +/- pixel buttons by the lower left corner of the grid allow you to change panel position at the chosen increment/decrement by selecting the datagrid cell (X-Pos, Y-Pos, Width, Height). You can also select "Always on Top" and "Hide Titlebar" if desire. Once all the panels are at their final position, just click "Lock Panel" to prevent further panel changes.
+ 6. Now, start the panel configuration by dragging the pop out panels into their final position (to your main monitor or other monitors). You can also type value directly into the data grid to move and resize a panel. The +/- pixel buttons by the lower left corner of the grid allow you to change panel position at the chosen increment/decrement by selecting the datagrid cell (X-Pos, Y-Pos, Width, Height). You can also select "Always on Top" and "Hide Titlebar" if desire. Once all the panels are at their final position, just click "Lock Panel" to prevent further panel changes.
-
+
-
-
-
+7. To test if everything is working. Once the profile is saved, please click "Restart" in the File menu. This will close all pop outs, except the built-in ones from the game main menu bar, and you're back to the start of the application. Now click "Start Pop Out" and see the magic happens!
- 7. To test if everything is working. Once the profile is saved, please click "Restart" in the File menu. This will close all pop out, except the built-in ones from the game main menu bar, and you're back to the start of the application. Now click "Start Pop Out" and see the magic happens!
-
-8. With auto panning feature enabled, you do not have to line up the circles that identified the panels in order for the panels to be popped out. But if you would like to do it manually without auto-panning, on next start of the flight, just line up the panels before clicking "Start Pop Out".
+8. With auto panning feature enabled, you do not have to line up the circles that identified the panels in order for the panels to be popped out. But if you would like to do it manually without auto-panning, on next start of the flight, just line up the panels before clicking "Start Pop Out" if needed.
@@ -87,25 +126,31 @@ The user plane profile data and application settings data are stored as JSON fil
* userdata/userprofiledata.json
* userdata/appsettingdata.json
-Note for technical user. If you would like to transfer existing profile data into v3.0 file format, you can first create a new profile in v3.0 of the app with the same panels and save it. Then you can open previous version of the configuration in config/userdata.json. You can match the old JSON attribute of "PanelDestinationList" of (Top, Left, Width, Height) for each panel and transfer it over to the new file JSON attribute of "PanelConfigs". Please edit at your own risk.
+**Note for technical user**. If you would like to shorten or extend the wait time for each step during Auto Pop Out, Panel, you can edit the following JSON element in appsettingdata.json file. The wait time is in seconds. If you don't see this JSON element, just update any File->Preferences setting and it will trigger this JSON element to be saved into the file.
+
+"AutoPopOutPanelsWaitDelay": {
+ "ReadyToFlyButton": 3,
+ "InitialCockpitView": 3,
+ "InstrumentationPowerOn": 2
+}
## Current Known Issue
* Sometimes when using the auto-panning feature, the keyboard combination of Ctrl-Alt-0 and Alt-0 do not work to save and load panel panning coordinates. First try to restart the flightsim and it usually fixes the problem. Otherwise, the only way to fix this is to redo the profile if you want the auto-panning feature since the camera angle is only being saved during the initial creation of the profile. The is another MSFS bug.
-* If running the game in windows mode on your non-primary monitor in a multi-monitor setup with different display resolution, panel identification and separation may not work correctly.
+
* Current application package size is bigger than previous version of the application because it is not a single EXE file package. With added feature of exception logging and stack trace to support user feedback and troubleshooting, a Single EXE package in .NET 5.0 as well as .NET 6.0 has a bug that stack trace information is not complete. Hopefully, Microsoft will be fixing this problem.
## Common Problem Resolution
-* Unable to pop out panels when creating a profile for the first time with error such as "Unable to pop out panel #X". If the panel is not being obstructed, by changing the sequence of the pop out when defining the profile may help solve the issue. Currently there are some panels in certain plane configuration that does not follow predefined MSFS pop out rule.
+* Unable to pop out panels when creating a profile for the first time with error such as "Unable to pop out panel #X". If the panel is not being obstructed by another window, by changing the sequence of the pop out when defining the profile may help solve the issue. Currently there are some panels in certain plane configuration that does not follow predefined MSFS pop out rule.
-* Unable to pop out panels on subsequent flight. Please follow status message instruction. Also, if using auto-panning, Ctrl-Alt-0 may not have been saved correctly during profile creation. You will be able to fix this by manually line up the panel circles identifier and do a force save view by pressing Ctrl-Alt-0.
+* Unable to pop out panels on subsequent flight. Please follow status message instruction. Also, if using auto-panning, Ctrl-Alt-0 may not have been saved correctly during profile creation. You can trigger a force camera angle save by clicking the "Save Auto Panning Camera" button for the profile.
* Unable to pop out ALL panels. This may indicate a potential miscount of selected panels (circles) and the number of actual panels that got popped out. You may have duplicate panels in your selection or panels that cannot be popped out.
-* If you encounter application crashes or unknown error, please help attach the file **error.log** in the application folder and open a ticket issue in the github repo for this project. This is going to help me troubleshoot the issue and provide hotfixes.
+* If you encounter application crashes or unknown error, please help my continuing development effort by attaching the file **error.log** in the application folder and open an issue ticket in github repo for this project. This is going to help me troubleshoot the issue and provide hotfixes.
## Author
@@ -115,13 +160,15 @@ Stanley Kwok
I welcome 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 your own project. But please abide by licensing terms and keep it open source:)
## Credits
-[Tesseract](https://github.com/charlesw/tesseract/) by Charles Weld - .NET wrapper for Tesseract OCR package. For version 1.x of application.
-
[AForge.NET](http://www.aforgenet.com/framework/) Image recognition library. For version 2.x of the application.
-[DarkUI](http://www.darkui.com/) by Robin Perria
-
[MouseKeyHook](https://github.com/gmamaladze/globalmousekeyhook) by George Mamaladze
[Fody](https://github.com/Fody/Fody) .NET assemblies weaver by Fody
+[MahApps.Metro Dark Theme](https://github.com/MahApps/MahApps.Metro) by Jan Karger, Dennis Daume, Brendan Forster, Paul Jenkins, Jake Ginnivan, Alex Mitchell
+
+[Hardcodet NotifyIcon](https://github.com/hardcodet/wpf-notifyicon) by Philipp Sumi, Robin Krom, Jan Karger
+
+[WPF CalcBinding](https://github.com/Alex141/CalcBinding) by Alexander Zinchenko
+
diff --git a/Shared/AppSettingData.cs b/Shared/AppSettingData.cs
deleted file mode 100644
index f277c4e..0000000
--- a/Shared/AppSettingData.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Newtonsoft.Json;
-
-namespace MSFSPopoutPanelManager.Shared
-{
- public class AppSettingData
- {
- public AppSettingData()
- {
- // Set defaults
- MinimizeToTray = false;
- AlwaysOnTop = true;
- UseAutoPanning = true;
- }
-
- public bool MinimizeToTray { get; set; }
-
- public bool AlwaysOnTop { get; set; }
-
- public bool UseAutoPanning { get; set; }
-
- [JsonIgnore]
- public bool AutoStart { get; set; }
- }
-}
diff --git a/Shared/BoolToVisibilityConverter.cs b/Shared/BoolToVisibilityConverter.cs
new file mode 100644
index 0000000..01c94c0
--- /dev/null
+++ b/Shared/BoolToVisibilityConverter.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace MSFSPopoutPanelManager.Shared
+{
+ [ValueConversion(typeof(bool), typeof(Visibility))]
+ public sealed class BoolToVisibilityConverter : IValueConverter
+ {
+ public Visibility TrueValue { get; set; }
+ public Visibility FalseValue { get; set; }
+
+ public BoolToVisibilityConverter()
+ {
+ // set defaults
+ TrueValue = Visibility.Visible;
+ FalseValue = Visibility.Collapsed;
+ }
+
+ public object Convert(object value, Type targetType,
+ object parameter, CultureInfo culture)
+ {
+ if (!(value is bool))
+ return null;
+ return (bool)value ? TrueValue : FalseValue;
+ }
+
+ public object ConvertBack(object value, Type targetType,
+ object parameter, CultureInfo culture)
+ {
+ if (Equals(value, TrueValue))
+ return true;
+ if (Equals(value, FalseValue))
+ return false;
+ return null;
+ }
+ }
+}
diff --git a/Shared/DataStore.cs b/Shared/DataStore.cs
deleted file mode 100644
index 412f90f..0000000
--- a/Shared/DataStore.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System.Linq;
-using System.ComponentModel;
-
-namespace MSFSPopoutPanelManager.Shared
-{
- public class DataStore : INotifyPropertyChanged
- {
- private int _activeProfileId;
-
- public event PropertyChangedEventHandler PropertyChanged;
-
- public DataStore()
- {
- _activeProfileId = -1;
- ActiveUserProfile = null;
- ActiveProfilePanelCoordinates = new BindingList();
- PanelConfigs = new BindingList();
-
- }
-
- public BindingList UserProfiles { get; set; }
-
- public BindingList ActiveProfilePanelCoordinates { get; set; }
-
- public BindingList PanelConfigs { get; set; }
-
- public UserProfileData ActiveUserProfile { get; set; }
-
- public int ActiveUserProfileId
- {
- get
- {
- return _activeProfileId;
- }
- set
- {
- _activeProfileId = value;
-
- if(value == -1)
- {
- ActiveUserProfile = null;
- ActiveProfilePanelCoordinates.Clear();
- }
- else
- {
- ActiveUserProfile = UserProfiles.ToList().Find(x => x.ProfileId == value);
- ActiveProfilePanelCoordinates.Clear();
- ActiveUserProfile.PanelSourceCoordinates.ForEach(c => ActiveProfilePanelCoordinates.Add(c));
- }
- }
- }
- }
-}
diff --git a/Shared/FileIO.cs b/Shared/FileIO.cs
new file mode 100644
index 0000000..9e724fd
--- /dev/null
+++ b/Shared/FileIO.cs
@@ -0,0 +1,13 @@
+using System.IO;
+
+namespace MSFSPopoutPanelManager.Shared
+{
+ public class FileIo
+ {
+ public static string GetUserDataFilePath()
+ {
+ var startupPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
+ return Path.Combine(startupPath, "userdata");
+ }
+ }
+}
diff --git a/Shared/Logger.cs b/Shared/Logger.cs
index 2179313..744068f 100644
--- a/Shared/Logger.cs
+++ b/Shared/Logger.cs
@@ -5,9 +5,8 @@ namespace MSFSPopoutPanelManager.Shared
public class Logger
{
public static event EventHandler> OnStatusLogged;
- public static event EventHandler> OnBackgroundStatusLogged;
- public static void Status(string message, StatusMessageType MessageType)
+ public static void LogStatus(string message, StatusMessageType MessageType)
{
var statusMessage = new StatusMessage() { Message = message, MessageType = MessageType };
OnStatusLogged?.Invoke(null, new EventArgs(statusMessage));
@@ -15,18 +14,7 @@ namespace MSFSPopoutPanelManager.Shared
public static void ClearStatus()
{
- Status(String.Empty, StatusMessageType.Info);
- }
-
- public static void BackgroundStatus(string message, StatusMessageType MessageType)
- {
- var statusMessage = new StatusMessage() { Message = message, MessageType = MessageType };
- OnBackgroundStatusLogged?.Invoke(null, new EventArgs(statusMessage));
- }
-
- public static void ClearBackgroundStatus()
- {
- BackgroundStatus(String.Empty, StatusMessageType.Info);
+ LogStatus(String.Empty, StatusMessageType.Info);
}
}
@@ -43,7 +31,7 @@ namespace MSFSPopoutPanelManager.Shared
Error
}
- public class PopoutManagerException : Exception
+ public class PopoutManagerException : Exception
{
public PopoutManagerException(string message) : base(message) { }
}
diff --git a/Shared/ObjectExtension.cs b/Shared/ObjectExtension.cs
new file mode 100644
index 0000000..9e1b316
--- /dev/null
+++ b/Shared/ObjectExtension.cs
@@ -0,0 +1,131 @@
+using MSFSPopoutPanelManager.Shared.ArrayExtensions;
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace MSFSPopoutPanelManager.Shared
+{
+ public static class ObjectExtensions
+ {
+ private static readonly MethodInfo CloneMethod = typeof(Object).GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance);
+
+ public static bool IsPrimitive(this Type type)
+ {
+ if (type == typeof(String)) return true;
+ return (type.IsValueType & type.IsPrimitive);
+ }
+
+ public static Object Copy(this Object originalObject)
+ {
+ return InternalCopy(originalObject, new Dictionary