From deea4bc739c3cef269a97cf769390817e81609f4 Mon Sep 17 00:00:00 2001 From: Stanley Date: Sat, 13 Aug 2022 02:14:49 -0400 Subject: [PATCH] Started development v3.4.2 --- Orchestration/FlightSimData.cs | 6 +- Orchestration/FlightSimOrchestrator.cs | 2 +- Orchestration/MainOrchestrator.cs | 3 +- .../PanelConfigurationOrchestrator.cs | 41 ++++-- Orchestration/PanelPopOutOrchestrator.cs | 12 +- Orchestration/ProfileData.cs | 14 +- Orchestration/ProfileOrchestrator.cs | 4 +- README.md | 28 ++-- .../kodiak/pfdmfd/PopoutPanelDefinition.json | 12 +- .../src/App/InteractiveControlTemplate.js | 47 +++---- ReactClient/src/App/MapPanel.js | 8 +- ReactClient/src/App/PopoutPanelContainer.js | 10 +- ReactClient/src/App/WebPanel.js | 9 +- .../src/Components/Control/MapDisplay.js | 108 ++++++++++++++- .../src/Services/LocalStorageProvider.js | 2 - .../src/Services/SimConnectDataProvider.js | 12 ++ SimconnectAgent/SimConnectProvider.cs | 9 +- SimconnectAgent/SimConnector.cs | 44 ++++-- SimconnectAgent/SimconnectAgent.csproj | 2 + SimconnectAgent/TouchPanel/DataProvider.cs | 25 ++++ .../TouchPanel/FlightPlanProvider.cs | 129 ++++++++++++++++++ .../TouchPanelSimConnectProvider.cs | 6 + .../TouchPanel/TouchPanelSimConnector.cs | 41 +++--- UserDataAgent/AppSetting.cs | 2 +- WebServer/Controllers/DataController.cs | 10 +- WebServer/SimConnectService.cs | 7 + WindowsAgent/WindowActionManager.cs | 26 +--- WpfApp/OnScreenMessageDialog.xaml.cs | 2 +- WpfApp/PanelCoorOverlay.xaml.cs | 2 +- WpfApp/PreferencesDialog.xaml | 6 +- WpfApp/Resources/info_icon.png | Bin 0 -> 2150 bytes WpfApp/UserControlPanelConfiguration.xaml | 16 ++- WpfApp/UserControlPanelConfiguration.xaml.cs | 3 + WpfApp/ViewModel/PanelSelectionViewModel.cs | 18 ++- WpfApp/WpfApp.csproj | 10 +- 35 files changed, 505 insertions(+), 171 deletions(-) create mode 100644 SimconnectAgent/TouchPanel/FlightPlanProvider.cs create mode 100644 WpfApp/Resources/info_icon.png diff --git a/Orchestration/FlightSimData.cs b/Orchestration/FlightSimData.cs index 63e78f0..1557bc5 100644 --- a/Orchestration/FlightSimData.cs +++ b/Orchestration/FlightSimData.cs @@ -7,6 +7,7 @@ namespace MSFSPopoutPanelManager.Orchestration public class FlightSimData : ObservableObject { public event PropertyChangedEventHandler CurrentMsfsAircraftChanged; + public event PropertyChangedEventHandler CurrentMsfsLiveryTitleChanged; public string CurrentMsfsAircraft { get; set; } @@ -27,10 +28,13 @@ namespace MSFSPopoutPanelManager.Orchestration { if (oldValue != newValue) { + base.OnPropertyChanged(propertyName, oldValue, newValue); + if (propertyName == "CurrentMsfsAircraft") CurrentMsfsAircraftChanged?.Invoke(this, null); - base.OnPropertyChanged(propertyName, oldValue, newValue); + if (propertyName == "CurrentMsfsLiveryTitle") + CurrentMsfsLiveryTitleChanged?.Invoke(this, null); } } diff --git a/Orchestration/FlightSimOrchestrator.cs b/Orchestration/FlightSimOrchestrator.cs index d5343cb..e82f2ba 100644 --- a/Orchestration/FlightSimOrchestrator.cs +++ b/Orchestration/FlightSimOrchestrator.cs @@ -61,7 +61,7 @@ namespace MSFSPopoutPanelManager.Orchestration if (FlightSimData.CurrentMsfsAircraft != aircraftName) { FlightSimData.CurrentMsfsAircraft = aircraftName; - ProfileData.AutoSwitchProfile(aircraftName); + ProfileData.AutoSwitchProfile(); } }; _simConnectProvider.OnFlightStarted += (sender, e) => OnFlightStarted?.Invoke(this, null); diff --git a/Orchestration/MainOrchestrator.cs b/Orchestration/MainOrchestrator.cs index 76475e4..b3a044b 100644 --- a/Orchestration/MainOrchestrator.cs +++ b/Orchestration/MainOrchestrator.cs @@ -20,7 +20,8 @@ namespace MSFSPopoutPanelManager.Orchestration TouchPanel = new TouchPanelOrchestrator(); FlightSimData = new FlightSimData(); - FlightSimData.CurrentMsfsAircraftChanged += (sernder, e) => ProfileData.RefreshProfile(); + FlightSimData.CurrentMsfsAircraftChanged += (sernder, e) => { ProfileData.RefreshProfile(); ProfileData.AutoSwitchProfile(); }; + FlightSimData.CurrentMsfsLiveryTitleChanged += (sernder, e) => { ProfileData.MigrateLiveryToAircraftBinding(); ProfileData.AutoSwitchProfile(); }; AppSettingData = new AppSettingData(); AppSettingData.AutoPopOutPanelsChanged += (sender, e) => FlightSim.AutoPanelPopOutActivation(e); diff --git a/Orchestration/PanelConfigurationOrchestrator.cs b/Orchestration/PanelConfigurationOrchestrator.cs index 0b2c86a..c46a65d 100644 --- a/Orchestration/PanelConfigurationOrchestrator.cs +++ b/Orchestration/PanelConfigurationOrchestrator.cs @@ -12,6 +12,7 @@ namespace MSFSPopoutPanelManager.Orchestration private static PInvoke.WinEventProc _winEvent; // keep this as static to prevent garbage collect or the app will crash private static IntPtr _winEventHook; private Rectangle _lastWindowRectangle; + private IntPtr _panelHandleDisableRefresh = IntPtr.Zero; public PanelConfigurationOrchestrator() { @@ -83,14 +84,17 @@ namespace MSFSPopoutPanelManager.Orchestration { case PanelConfigPropertyName.Left: case PanelConfigPropertyName.Top: - WindowActionManager.MoveWindow(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); + _panelHandleDisableRefresh = panelConfig.PanelHandle; + WindowActionManager.MoveWindow(panelConfig.PanelHandle, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); break; case PanelConfigPropertyName.Width: case PanelConfigPropertyName.Height: + _panelHandleDisableRefresh = panelConfig.PanelHandle; + if (panelConfig.HideTitlebar) WindowActionManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, false); - WindowActionManager.MoveWindowWithMsfsBugOverrirde(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); + WindowActionManager.MoveWindowWithMsfsBugOverrirde(panelConfig.PanelHandle, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); if (panelConfig.HideTitlebar) WindowActionManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, true); @@ -100,6 +104,7 @@ namespace MSFSPopoutPanelManager.Orchestration WindowActionManager.ApplyAlwaysOnTop(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.AlwaysOnTop, new Rectangle(panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height)); break; case PanelConfigPropertyName.HideTitlebar: + _panelHandleDisableRefresh = panelConfig.PanelHandle; WindowActionManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, panelConfig.HideTitlebar); break; case PanelConfigPropertyName.TouchEnabled: @@ -136,32 +141,36 @@ namespace MSFSPopoutPanelManager.Orchestration switch (configPropertyName) { case PanelConfigPropertyName.Left: + _panelHandleDisableRefresh = panelConfig.PanelHandle; panelConfig.Left += changeAmount; - WindowActionManager.MoveWindow(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); + WindowActionManager.MoveWindow(panelConfig.PanelHandle, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); break; case PanelConfigPropertyName.Top: + _panelHandleDisableRefresh = panelConfig.PanelHandle; panelConfig.Top += changeAmount; - WindowActionManager.MoveWindow(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); + WindowActionManager.MoveWindow(panelConfig.PanelHandle, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); break; case PanelConfigPropertyName.Width: + _panelHandleDisableRefresh = panelConfig.PanelHandle; panelConfig.Width += changeAmount; if (panelConfig.HideTitlebar) WindowActionManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, false); - WindowActionManager.MoveWindowWithMsfsBugOverrirde(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); + WindowActionManager.MoveWindowWithMsfsBugOverrirde(panelConfig.PanelHandle, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); if (panelConfig.HideTitlebar) WindowActionManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, true); break; case PanelConfigPropertyName.Height: + _panelHandleDisableRefresh = panelConfig.PanelHandle; panelConfig.Height += changeAmount; if (panelConfig.HideTitlebar) WindowActionManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, false); - WindowActionManager.MoveWindowWithMsfsBugOverrirde(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); + WindowActionManager.MoveWindowWithMsfsBugOverrirde(panelConfig.PanelHandle, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); if (panelConfig.HideTitlebar) WindowActionManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, true); @@ -224,7 +233,7 @@ namespace MSFSPopoutPanelManager.Orchestration { case PInvokeConstant.EVENT_SYSTEM_MOVESIZEEND: // Move window back to original location - WindowActionManager.MoveWindow(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); + WindowActionManager.MoveWindow(panelConfig.PanelHandle, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); break; case PInvokeConstant.EVENT_OBJECT_LOCATIONCHANGE: WINDOWPLACEMENT wp = new WINDOWPLACEMENT(); @@ -249,21 +258,25 @@ namespace MSFSPopoutPanelManager.Orchestration return; _lastWindowRectangle = winRectangle; - Rectangle clientRectangle; - PInvoke.GetClientRect(panelConfig.PanelHandle, out clientRectangle); + + if (_panelHandleDisableRefresh != IntPtr.Zero) + { + _panelHandleDisableRefresh = IntPtr.Zero; + return; + } panelConfig.Left = winRectangle.Left; panelConfig.Top = winRectangle.Top; - if (panelConfig.HideTitlebar) + if (!panelConfig.HideTitlebar) { - panelConfig.Width = clientRectangle.Width; - panelConfig.Height = clientRectangle.Height; + panelConfig.Width = winRectangle.Width - winRectangle.Left; + panelConfig.Height = winRectangle.Height - winRectangle.Top; } else { - panelConfig.Width = clientRectangle.Width + 16; - panelConfig.Height = clientRectangle.Height + 39; + panelConfig.Width = winRectangle.Width - winRectangle.Left - 16; + panelConfig.Height = winRectangle.Height - winRectangle.Top - 39; } // Detect if window is maximized, if so, save settings diff --git a/Orchestration/PanelPopOutOrchestrator.cs b/Orchestration/PanelPopOutOrchestrator.cs index 3b4a94a..05b4afa 100644 --- a/Orchestration/PanelPopOutOrchestrator.cs +++ b/Orchestration/PanelPopOutOrchestrator.cs @@ -55,7 +55,7 @@ namespace MSFSPopoutPanelManager.Orchestration if (ActiveProfile == null) return; - ProfileData.AutoSwitchProfile(FlightSimData.CurrentMsfsAircraft); + ProfileData.AutoSwitchProfile(); FlightSimData.IsEnteredFlight = true; @@ -198,7 +198,7 @@ namespace MSFSPopoutPanelManager.Orchestration { if (panelConfigs[i].PanelType == PanelType.CustomPopout) { - WindowActionManager.MoveWindow(panelConfigs[i].PanelHandle, panelConfigs[i].PanelType, panelConfigs[i].Top, panelConfigs[i].Left, panelConfigs[i].Width, panelConfigs[i].Height); + WindowActionManager.MoveWindow(panelConfigs[i].PanelHandle, panelConfigs[i].Top, panelConfigs[i].Left, panelConfigs[i].Width, panelConfigs[i].Height); PInvoke.SetForegroundWindow(panelConfigs[i].PanelHandle); Thread.Sleep(200); } @@ -237,7 +237,7 @@ namespace MSFSPopoutPanelManager.Orchestration // Need to move the window to upper left corner first. There is a possible bug in the game that panel pop out to full screen that prevents further clicking. if (handle != IntPtr.Zero) - WindowActionManager.MoveWindow(handle, PanelType.CustomPopout, 0, 0, 800, 600); + WindowActionManager.MoveWindow(handle, 0, 0, 800, 600); // Make window always on top to make sure it is clickable and not obstruct by other user windows //WindowActionManager.ApplyAlwaysOnTop(handle, PanelType.WPFWindow, true); @@ -260,7 +260,7 @@ namespace MSFSPopoutPanelManager.Orchestration } // Fix SU10+ bug where pop out window after separation is huge - WindowActionManager.MoveWindow(handle, PanelType.CustomPopout, -8, 0, 800, 600); + WindowActionManager.MoveWindow(handle, -8, 0, 800, 600); var panel = new PanelConfig(); panel.PanelHandle = handle; @@ -296,7 +296,7 @@ namespace MSFSPopoutPanelManager.Orchestration // MSFS draws popout panel differently at different time for same panel // ToDo: Need to figure mouse click code to separate window - WindowActionManager.MoveWindow(hwnd, PanelType.CustomPopout, -8, 0, 800, 600); + WindowActionManager.MoveWindow(hwnd, -8, 0, 800, 600); PInvoke.SetForegroundWindow(hwnd); Thread.Sleep(500); @@ -410,7 +410,7 @@ namespace MSFSPopoutPanelManager.Orchestration { PInvoke.ShowWindow(panel.PanelHandle, PInvokeConstant.SW_RESTORE); Thread.Sleep(250); - WindowActionManager.MoveWindow(panel.PanelHandle, panel.PanelType, panel.Left, panel.Top, panel.Width, panel.Height); + WindowActionManager.MoveWindow(panel.PanelHandle, panel.Left, panel.Top, panel.Width, panel.Height); Thread.Sleep(1000); } diff --git a/Orchestration/ProfileData.cs b/Orchestration/ProfileData.cs index 04e71bc..71c899d 100644 --- a/Orchestration/ProfileData.cs +++ b/Orchestration/ProfileData.cs @@ -160,12 +160,12 @@ namespace MSFSPopoutPanelManager.Orchestration UpdateActiveProfile(currentProfileId); } - public void AutoSwitchProfile(string activeAircraft) + public void AutoSwitchProfile() { // Automatic switching of active profile when SimConnect active aircraft changes - if (Profiles != null) + if (Profiles != null && !string.IsNullOrEmpty(FlightSimData.CurrentMsfsAircraft)) { - var matchedProfile = Profiles.FirstOrDefault(p => p.BindingAircrafts.Any(t => t == activeAircraft)); + var matchedProfile = Profiles.FirstOrDefault(p => p.BindingAircrafts.Any(t => t == FlightSimData.CurrentMsfsAircraft)); if (matchedProfile != null) UpdateActiveProfile(matchedProfile.ProfileId); } @@ -175,15 +175,21 @@ namespace MSFSPopoutPanelManager.Orchestration // Started in v3.4.2 public void MigrateLiveryToAircraftBinding(string liveryName, string aircraftName) { - if (Profiles != null) + if (Profiles != null && !string.IsNullOrEmpty(liveryName) && !string.IsNullOrEmpty(aircraftName)) { var matchedProfile = Profiles.FirstOrDefault(p => p.BindingAircraftLiveries.Any(t => t == liveryName)); if (matchedProfile != null && !matchedProfile.BindingAircrafts.Any(a => a == aircraftName)) { matchedProfile.BindingAircrafts.Add(aircraftName); WriteProfiles(); + RefreshProfile(); } } } + + public void MigrateLiveryToAircraftBinding() + { + MigrateLiveryToAircraftBinding(FlightSimData.CurrentMsfsLiveryTitle, FlightSimData.CurrentMsfsAircraft); + } } } diff --git a/Orchestration/ProfileOrchestrator.cs b/Orchestration/ProfileOrchestrator.cs index 86f9c35..2b7c41e 100644 --- a/Orchestration/ProfileOrchestrator.cs +++ b/Orchestration/ProfileOrchestrator.cs @@ -40,13 +40,13 @@ namespace MSFSPopoutPanelManager.Orchestration public void AddProfileBinding(string bindingAircraft) { - if (ProfileData.ActiveProfile != null) + if (ProfileData.ActiveProfile != null && bindingAircraft != null) ProfileData.AddProfileBinding(bindingAircraft, ProfileData.ActiveProfile.ProfileId); } public void DeleteProfileBinding(string bindingAircraft) { - if (ProfileData.ActiveProfile != null) + if (ProfileData.ActiveProfile != null && bindingAircraft != null) ProfileData.DeleteProfileBinding(bindingAircraft, ProfileData.ActiveProfile.ProfileId); } } diff --git a/README.md b/README.md index bac579e..a17e3f3 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Please follow [FlightSimulator.com](https://forums.flightsimulator.com/t/msfs-po
## Touch Panel Feature -With SU10 Beta v1.27.11, Asobo seems to have fix a major [bug](#touch-enable-pop-out-feature) (in SU9 and before) that stops Pop Out Manager's touch panel feature from working reliably. I'm happy to announce touch enabled feature works pretty well out of the box on either direct connected touch monitor or on pop out window that is displayed on tablet using software tool such as SpaceDesk. Until Asobo actually allow touch passthrough for panels, this tool can serve as a stopgap solution. +With SU10 Beta v1.27.11, Asobo seems to have fix a major [bug](#touch-enable-pop-out-feature) (in SU9 and before) that stops Pop Out Panel Manager's touch panel feature from working reliably. I'm happy to announce touch enabled feature works pretty well out of the box on either direct connected touch monitor or on pop out window that is displayed on tablet using software tool such as SpaceDesk. Until Asobo actually allow touch passthrough for panels, this tool can serve as a stopgap solution. I've tested touch operation on GTN750, GTN530, KingAir PFD/MFD, TMB 930 FMS, Flybywire A32NX EFB and they are operational. Please report any issues that you encounter when using touch enable feature. There is still lots of room for improvement and I'll continue my effort to make touch work better and better. @@ -28,8 +28,6 @@ Things that work out of the box: If using SpaceDesk to host pop out panel display, since there is a latency for touch response in wireless display, your touch may not register consistently. Please go to Preferences => Touch Settings => Touch Down Touch Up Delay, and increase the value to 25ms or higher to increase touch sensitivity. - - ## Application Features * Display resolution independent. Supports 1080p/1440p/4k display and ultrawide displays. @@ -55,7 +53,7 @@ If using SpaceDesk to host pop out panel display, since there is a latency for t * Auto update feature. Application can auto-update itself when new version becomes available. -* **Experimental Feature**: Enable touch support for pop outs on touch capable display. Please see [Touch Enable Pop Out Feature](#touch-enable-pop-out-feature) for more information. +* Enable touch support for pop outs on touch capable display. Please see [Touch Enable Pop Out Feature](#touch-enable-pop-out-feature) for more information.
@@ -96,7 +94,7 @@ What if you can do the setup once by defining on screen where the pop out panels

-2. For step 2, if you want to associate the profile to the current aircraft to use in [Auto Pop Out](#auto-pop-out-feature) feature or for automatic profile switching when selecting a different aircraft, click the "plus" button next to the aircraft name. The aircraft title will become green once the it is bound to the profile. Your chosen aircraft may not be available to select in the application for your newly selected plane until a flight is started. +2. If you want to associate the profile to the current aircraft to use in [Auto Pop Out](#auto-pop-out-feature) feature or for automatic profile switching when selecting a different aircraft, click the "plus" button next to the aircraft name. The aircraft title will become green once it is bound to the profile. Your chosen aircraft may not be available to select in the application for your newly selected plane until a flight is started. If the current aircraft has not been bound to another profile, it will be bound to your newly created profile automatically.

@@ -138,11 +136,9 @@ The app will try to find a matching profile with the current selected aircraft. * In File->Preferences->Auto Pop Out Panel Settings, "Enable Auto Pop Out Panels" option is turned on. You can also adjust wait delay settings if you've a fast computer. -* For existing profile to use Auto Pop Out feature, just click the plus sign in the bind active aircraft to profile section. This will bind the active aircraft being displayed to the profile. Any bound aircraft will appear in GREEN color. Unbound ones will be in WHITE, and bound aircraft in another profile will be in RED. You can bind as many liveries to a profile as you desire but an aircraft can only bind to a single profile so Auto Pop Out knows which profile to start when a flight session starts. +* For existing profile to use Auto Pop Out feature, just click the plus sign in the bind active aircraft to profile section. This will bind the active aircraft being displayed to the profile. Any bound aircraft will appear in GREEN color. Unbound ones will be in WHITE, and bound aircraft in another profile will be in RED. You can bind as many aircrafts to a profile as you desire but an aircraft can only bind to a single profile so Auto Pop Out knows which profile to start when a flight session starts. -* 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. - -* **TIPS:** You can go to the preference settings to configure the time delay for each steps for the Auto Pop Out process based on the speed of your computer if things do not work correctly or if you want to speed up the Auto 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. * **TIPS:** One trick to force SimConnect to update the current selected aircraft so you can do the binding immediately after selecting a new aircraft in the World Map is to click the "Go Back" button at the lower left of your screen. You should see aircraft title changes to the active ones. Or you can wait until the flight is almost fully loaded and you will see the current aircraft name gets updated. @@ -166,28 +162,22 @@ In MSFS, when operating the above panels with pop outs, there are currently 2 li - Limitation #2 - When you click or hover your mouse over any pop out panels on your main monitor or on another monitor, the game main window will lose focus and you can’t operate flight control without clicking the game window again. -**EDITED (July 31st)** - Asobo have fixed the below bug in SU10 Beta v1.27.11. And hopefuly it won't be broken again. +**EDITED (July 31st)** - Asobo have fixed the below bug in SU10 Beta v1.27.11. And hopefully it won't be broken again. - ~~Bug - If the pop out panel is also display on the main screen, a click through at incorrect coordinate will occur at the relative position where the pop out panel is located. If you click at this particular location in the pop out panel, the click event will register at the wrong coordinate. I haven’t been able to figure out how to work around this issue yet since the bug is deep in the closed source CoherentGT code in how MSFS implements the internal browser control to display to pop out panel. So touch will not work in the relative position of the pop out panel where panel appears on the main screen. This only affects instrumentation pop outs. The built-in ones such as ATC and checklist are fine since once they’re popped out, they no longer appear on the main screen. Below is the screenshot where click through at incorrect coordinate occurs. See the relative position (red box) in the pop out where the same instrumentation appears on the main screen.~~

- If you're a home cockpit builder and your main screen has a view that looks like something below, than touch enable feature will work almost 100% of the time since there is no click through target on your main screen. - -

- -

- #### How to enable touch support Perform your regular panel selection and once your touch capable panel has been popped out, in the configuration screen grid, just check "Touch Enabled" to enable touch support for the selected panel. #### Known Issues -~~- A MSFS click through bug where pop out panel also appears on the main game screen. Touch will not register correctly in the section of the pop out panel where the relative position of the panel corresponds to where the panel is located on the main game screen.~~ **Asobo fixed in SU10 Beta 1.27.11.** +~~- A MSFS click through bug where pop out panel also appears on the main game screen. Touch will not register correctly in the section of the pop out panel where the relative position of the panel corresponds to where the panel is located on the main game screen.~~ **Fixed in SU10 Beta 1.27.11.** -~~- When a click through occurs on non-instrumentation panel items such as throttle or switches, even though the switches will not accidentally get clicked, touch response in pop out panel may not work. Just touching a little bit to the left/right in the pop out panel may register your touch event correctly and trigger your intend target.~~ **Asobo fixed in SU10 Beta 1.27.11.** +~~- When a click through occurs on non-instrumentation panel items such as throttle or switches, even though the switches will not accidentally get clicked, touch response in pop out panel may not work. Just touching a little bit to the left/right in the pop out panel may register your touch event correctly and trigger your intend target.~~ **Fixed in SU10 Beta 1.27.11.** - If touch suddenly becomes unresponsive, please try to change the main view of the game such as looking left/right using keyboard shortcut. This will sometime reset the mouse coordinate where you touch the pop out panel. @@ -239,7 +229,7 @@ You can backup this folder and restore this folder if you want to uninstall and ERROR MSFSPopoutPanelManager.WpfApp.App - Could not load file or assembly 'Microsoft.FlightSimulator.SimConnect, Version=11.0.62651.3, Culture=neutral, PublicKeyToken=baf445ffb3a06b5c'. An attempt was made to load a program with an incorrect format. System.BadImageFormatException: - This usually happens on clean Windows installation. Pop Out Panel Manager uses x64 version of SimConnect.dll to perform its function and a Visaul C++ redistributable is required for SimConnect to run correctly. Please download and install the following [VC++ redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe) on your PC to resolve this issue. Further information can be obtained in this [support ticket](https://github.com/hawkeye-stan/msfs-popout-panel-manager/issues/21). + This usually happens on clean Windows installation. Pop Out Panel Manager uses x64 version of SimConnect.dll to perform its function and a Visual C++ redistributable is required for SimConnect to run correctly. Please download and install the following [VC++ redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe) on your PC to resolve this issue. Further information can be obtained in this [support ticket](https://github.com/hawkeye-stan/msfs-popout-panel-manager/issues/21). ## Author Stanley Kwok diff --git a/ReactClient/public/config/profiles/kodiak/pfdmfd/PopoutPanelDefinition.json b/ReactClient/public/config/profiles/kodiak/pfdmfd/PopoutPanelDefinition.json index c501ed8..7fbb475 100644 --- a/ReactClient/public/config/profiles/kodiak/pfdmfd/PopoutPanelDefinition.json +++ b/ReactClient/public/config/profiles/kodiak/pfdmfd/PopoutPanelDefinition.json @@ -343,7 +343,7 @@ "controlSize": { "$ref": "#value.controlSize.regularKnob" }, - "highlight": false, + "highlight": true, "action": { "touchActions": [ { @@ -571,7 +571,7 @@ "controlSize": { "$ref": "#value.controlSize.speedKnob" }, - "highlight": false, + "highlight": true, "action": { "touchActions": [ { @@ -1589,7 +1589,7 @@ "controlSize": { "$ref": "#value.controlSize.regularKnob" }, - "highlight": false, + "highlight": true, "action": { "touchActions": [ { @@ -1814,7 +1814,7 @@ "controlSize": { "$ref": "#value.controlSize.speedKnob" }, - "highlight": false, + "highlight": true, "action": { "touchActions": [ { @@ -2568,7 +2568,7 @@ "controlSize": { "$ref": "#value.controlSize.regularKnob" }, - "highlight": false, + "highlight": true, "action": { "touchActions": [ { @@ -2793,7 +2793,7 @@ "controlSize": { "$ref": "#value.controlSize.speedKnob" }, - "highlight": false, + "highlight": true, "action": { "touchActions": [ { diff --git a/ReactClient/src/App/InteractiveControlTemplate.js b/ReactClient/src/App/InteractiveControlTemplate.js index 12aa0f5..a2c5852 100644 --- a/ReactClient/src/App/InteractiveControlTemplate.js +++ b/ReactClient/src/App/InteractiveControlTemplate.js @@ -14,7 +14,7 @@ const useStyles = makeStyles(() => ({ height: '100%' }, iconImageHighlight: { - filter: 'sepia(80%)' + filter: 'brightness(1.5) sepia(1)' }, controlBase: { position: 'absolute', @@ -83,7 +83,6 @@ const setupControlWidthHeightStyle = (ctrl, panelInfo) => { const ImageControl = ({ctrl, panelInfo}) => { const classes = useStyles(); const imagePath = getImagePath(panelInfo); - const isHighlighted = useRef(false); const setupBackgroundImageStyle = () => { if (ctrl.image === undefined) { @@ -95,18 +94,17 @@ const ImageControl = ({ctrl, panelInfo}) => { } return useMemo(() =>( -
+
- ), [ctrl, isHighlighted.current]) + ), [ctrl]) } -const ImageButton = ({ctrl, panelInfo, showEncoder}) => { +const ImageButton = ({ctrl, panelInfo, showEncoder, highLightedControlId, highlightedControlChanged}) => { const classes = useStyles(); const { simConnectData } = useSimConnectData(); const { isUsedArduino, isEnabledSound } = useLocalStorageData().configurationData; const imagePath = getImagePath(panelInfo); - const isHighlighted = useRef(false); const setupBackgroundImageStyle = () => { if (ctrl.image === undefined) { @@ -118,10 +116,10 @@ const ImageButton = ({ctrl, panelInfo, showEncoder}) => { } const handleOnClick = (event) => { - if (ctrl.highlight === undefined || ctrl.highlight) { - isHighlighted.current = true; - setTimeout(() => { isHighlighted.current = false; }, 2000); - } + if (ctrl.highlight === undefined || ctrl.highlight && highlightedControlChanged !== null) + highlightedControlChanged(ctrl.id); + else + highlightedControlChanged(null); if (ctrl.action != null) playSound(isEnabledSound); @@ -133,18 +131,17 @@ const ImageButton = ({ctrl, panelInfo, showEncoder}) => { } return useMemo(() =>( -
+
handleOnClick(event)} />
- ), [ctrl, isHighlighted.current, isUsedArduino, isEnabledSound]) + ), [ctrl, isUsedArduino, isEnabledSound, highLightedControlId]) } -const BindableImageButton = ({ctrl, panelInfo, showEncoder}) => { +const BindableImageButton = ({ctrl, panelInfo, showEncoder, highLightedControlId, highlightedControlChanged}) => { const classes = useStyles(); const { simConnectData } = useSimConnectData(); const { isUsedArduino, isEnabledSound } = useLocalStorageData().configurationData; const imagePath = getImagePath(panelInfo); - const isHighlighted = useRef(false); const dataBindingValue = simConnectData[ctrl.binding?.variable]; const setupBackgroundImageStyle = () => { @@ -171,10 +168,10 @@ const BindableImageButton = ({ctrl, panelInfo, showEncoder}) => { } const handleOnClick = (event) => { - if (ctrl.highlight === undefined || ctrl.highlight) { - isHighlighted.current = true; - setTimeout(() => { isHighlighted.current = false; }, 2000); - } + if (ctrl.highlight === undefined || ctrl.highlight && highlightedControlChanged !== null) + highlightedControlChanged(ctrl.id); + else + highlightedControlChanged(null); if (ctrl.action != null) playSound(isEnabledSound); @@ -186,10 +183,10 @@ const BindableImageButton = ({ctrl, panelInfo, showEncoder}) => { } return useMemo(() =>( -
+
handleOnClick(event)} />
- ), [ctrl, dataBindingValue, isHighlighted.current, isUsedArduino, isEnabledSound]) + ), [ctrl, dataBindingValue, isUsedArduino, isEnabledSound, highLightedControlId]) } const DigitDisplay = ({ctrl, panelInfo}) => { @@ -248,7 +245,7 @@ const TextBlock = ({ctrl, panelInfo}) => { } -const InteractiveControlTemplate = ({ ctrl, panelInfo, showEncoder }) => { +const InteractiveControlTemplate = ({ ctrl, panelInfo, showEncoder, highLightedControlId, highlightedControlChanged }) => { // preload all dynamic bindable images for control useEffect(() => { let imagePath = getImagePath(panelInfo); @@ -263,16 +260,16 @@ const InteractiveControlTemplate = ({ ctrl, panelInfo, showEncoder }) => { } }, []) - return ( + return useMemo(() =>( <> {ctrl.type === 'image' && } {ctrl.type === 'imageButton' && - + } {ctrl.type === 'bindableImageButton' && - + } {ctrl.type === 'digitDisplay' && @@ -284,7 +281,7 @@ const InteractiveControlTemplate = ({ ctrl, panelInfo, showEncoder }) => { } - ) + ), [highLightedControlId]) } export default InteractiveControlTemplate; diff --git a/ReactClient/src/App/MapPanel.js b/ReactClient/src/App/MapPanel.js index efe6317..81a3b2f 100644 --- a/ReactClient/src/App/MapPanel.js +++ b/ReactClient/src/App/MapPanel.js @@ -1,4 +1,4 @@ -import React, {useState, useEffect} from 'react'; +import React, {useState, useEffect, useMemo} from 'react'; import makeStyles from '@mui/styles/makeStyles'; import { MapContainer } from 'react-leaflet'; import { useSimConnectData } from '../Services/SimConnectDataProvider'; @@ -20,7 +20,7 @@ const MapPanel = ({refresh}) => { const { simConnectSystemEvent } = useSimConnectData(); const classes = useStyles(); const [reload, setReload] = useState(true); - + useEffect(() =>{ if(simConnectSystemEvent !== null) { @@ -31,7 +31,7 @@ const MapPanel = ({refresh}) => { } }, [simConnectSystemEvent]) - return ( + return useMemo(() => (
{ reload && @@ -39,7 +39,7 @@ const MapPanel = ({refresh}) => { }
- ) + ), [refresh]) } export default MapPanel; diff --git a/ReactClient/src/App/PopoutPanelContainer.js b/ReactClient/src/App/PopoutPanelContainer.js index 351986f..00857e7 100644 --- a/ReactClient/src/App/PopoutPanelContainer.js +++ b/ReactClient/src/App/PopoutPanelContainer.js @@ -28,7 +28,7 @@ const useStyles = makeStyles((theme) => ({ } })); -const PopoutPanelContainer = ({panelInfo}) => { +const PopoutPanelContainer = ({panelInfo, highLightedControlId, highlightedControlChanged}) => { const { simConnectSystemEvent } = useSimConnectData(); const { isUsedArduino } = useLocalStorageData().configurationData; const classes = useStyles(panelInfo); @@ -78,9 +78,11 @@ const PopoutPanelContainer = ({panelInfo}) => { key={ctrl.id} ctrl={ctrl} panelInfo={panelInfo} - showEncoder={(e, useDualEncoder) => handleShowEncoder(e, useDualEncoder)}> + showEncoder={(e, useDualEncoder) => handleShowEncoder(e, useDualEncoder)} + highLightedControlId={highLightedControlId} + highlightedControlChanged={(ctrlId) => highlightedControlChanged(ctrlId)} + > - )}
} @@ -94,7 +96,7 @@ const PopoutPanelContainer = ({panelInfo}) => { }
- ), [panelInfo, keyPadOpen, isUsedArduino]) + ), [panelInfo, keyPadOpen, isUsedArduino, highLightedControlId]) } export default PopoutPanelContainer; \ No newline at end of file diff --git a/ReactClient/src/App/WebPanel.js b/ReactClient/src/App/WebPanel.js index eafa488..952e598 100644 --- a/ReactClient/src/App/WebPanel.js +++ b/ReactClient/src/App/WebPanel.js @@ -67,6 +67,7 @@ const WebPanel = ({ planeId, panelId }) => { const classes = useStyles(useWindowDimensions())(); const [mapOpen, setMapOpen] = useState(false); const [panelProfile, setPanelProfile] = useState(); + const [highLightedControlId, setHighLightedControlId] = useState(null); document.body.style.backgroundColor = 'transparent'; @@ -128,9 +129,13 @@ const WebPanel = ({ planeId, panelId }) => {
- {panelProfile.subPanels.map(subPanel => + {!mapOpen && panelProfile.subPanels.map(subPanel =>
- + setHighLightedControlId(ctrlId)} + />
)}
diff --git a/ReactClient/src/Components/Control/MapDisplay.js b/ReactClient/src/Components/Control/MapDisplay.js index 4f27dea..be46d40 100644 --- a/ReactClient/src/Components/Control/MapDisplay.js +++ b/ReactClient/src/Components/Control/MapDisplay.js @@ -1,13 +1,14 @@ import React, { useEffect, useState, useRef, useMemo } from 'react'; -import { useSimConnectData } from '../../Services/SimConnectDataProvider'; +import { useSimConnectData, simConnectGetFlightPlan } from '../../Services/SimConnectDataProvider'; import { useLocalStorageData } from '../../Services/LocalStorageProvider'; import { useInterval } from '../Util/hooks'; -import { LayersControl, TileLayer, useMapEvents } from 'react-leaflet' +import { LayersControl, LayerGroup, TileLayer, useMapEvents } from 'react-leaflet' import L from 'leaflet'; import 'leaflet.marker.slideto'; import 'leaflet-marker-rotation'; import 'leaflet-easybutton'; import '@elfalem/leaflet-curve'; +import { getDistance } from 'geolib'; import {BingLayer} from 'react-leaflet-bing-v2'; const MAP_TYPE_DEFAULTS = { zoomLevel: 12, flightFollowing: true, showFlightPlan: true, uiZoomFactor: 1, planeRadiusCircleRange: 2.5}; @@ -74,6 +75,82 @@ const drawPlaneCircleRadius = (map, planePosition, scaleInNm = 2.5, isCreated, m return marker; } +const getControlPoints = (waypoints) => { + // This is to create two control points XXXX meters away any give waypoint to give flight path + // a smooth quadratic bezier curve transition. You can adjust the bezier curve radius with the constant below + const bezierCurveRadius = 100; + + let controlPoints = []; + for (var i = 0; i < waypoints.length; i++) { + let prevWp = i === 0 ? null : waypoints[i - 1].latLong; + let curWp = waypoints[i].latLong; + let nextWp = i === waypoints.length - 1 ? null : waypoints[i + 1].latLong; + + var distance1 = prevWp === null ? null : getDistance({ latitude: prevWp[0], longitude: prevWp[1] }, { latitude: curWp[0], longitude: curWp[1] }); + var distance2 = nextWp === null ? null : getDistance({ latitude: curWp[0], longitude: curWp[1] }, { latitude: nextWp[0], longitude: nextWp[1] }); + + let ratio1 = (distance1 / bezierCurveRadius) > 2 ? (distance1 / bezierCurveRadius) : 2; + let ratio2 = (distance2 / bezierCurveRadius) > 2 ? (distance2 / bezierCurveRadius) : 2; + + var p1 = prevWp === null ? null : [(prevWp[0] - curWp[0]) / ratio1 + curWp[0], (prevWp[1] - curWp[1]) / ratio1 + curWp[1]]; + var p2 = nextWp === null ? null : [(nextWp[0] - curWp[0]) / ratio2 + curWp[0], (nextWp[1] - curWp[1]) / ratio2 + curWp[1]]; + + controlPoints.push({ p1: p1, p2: p2 }); + } + + return controlPoints; +} + +const drawFlightPath = (waypoints, layerGroup, scaleInNm) => { + let scale = 2.5 / scaleInNm; + let path, line, marker, tooltip; + let controlPoints = getControlPoints(waypoints); + + waypoints.forEach((waypoint, index) => { + let controlPoint = controlPoints[index]; + let nextControlPoint = controlPoints[index + 1]; + + let lineColor = Boolean(waypoint.isActiveLeg) ? 'magenta' : 'magenta' + + // First waypoint + if (controlPoint.p1 === null) { + path = [waypoint.latLong, controlPoint.p2]; + line = new L.Polyline(path, {color: lineColor}); + layerGroup.addLayer(line); + } + // Last waypoint + else if (controlPoint.p2 === null) { + path = [controlPoint.p1, waypoint.latLong]; + line = new L.Polyline(path, {color: lineColor}); + layerGroup.addLayer(line); + } + // All other waypoints inbetween, draw bezier curve + else { + path = ['M', controlPoint.p1, 'Q', waypoint.latLong, controlPoint.p2]; + line = L.curve(path, {color: 'white'}); + layerGroup.addLayer(line); + } + + // Waypoint marker + tooltip = getFullmapTooltip(waypoint); + marker = L.circleMarker(waypoint.latLong, {radius: 6, color: 'purple'}).bindTooltip(tooltip, { permanent: true, interactive: true, offset: [-50 * scale, -15 * scale] }); + + layerGroup.addLayer(marker); + + // Draw inbetween control points line + if (index < waypoints.length - 1) { + path = [controlPoint.p2, nextControlPoint.p1]; + line = new L.Polyline(path, {color: lineColor}); + layerGroup.addLayer(line); + } + }) +} + +const getFullmapTooltip = (waypoint) => { + let tooltip = `
${waypoint.id}
`; + return tooltip; +} + const centerPlaneToMap = (map, mapPosition, planePosition) => { mapPosition.current = planePosition.current; if (mapPosition.current !== null) @@ -103,7 +180,7 @@ const formatLatLong = (lat, lon) => { let centerPlaneIcon, flightFollowingIcon, showFlightPlanIcon; const MapDisplay = ({refresh}) => { - const { simConnectData } = useSimConnectData(); + const { simConnectData, simConnectSystemEvent } = useSimConnectData(); const { PLANE_HEADING_TRUE, GPS_LAT, GPS_LON } = simConnectData; const { mapConfig, configurationData } = useLocalStorageData(); const { mapRefreshInterval } = configurationData; @@ -111,6 +188,7 @@ const MapDisplay = ({refresh}) => { const [ mapDefaults ] = useState(MAP_TYPE_DEFAULTS); const [ flightFollowing, setFlightFollowing ] = useState(MAP_TYPE_DEFAULTS.flightFollowing); const [ showFlightPlan, setShowFlightPlan ] = useState(MAP_TYPE_DEFAULTS.showFlightPlan); + const [ waypoints, setWaypoints] = useState([]); const planePosition = useRef(formatLatLong(GPS_LAT, GPS_LON)); const mapPosition = useRef(formatLatLong(GPS_LAT, GPS_LON)); @@ -236,6 +314,27 @@ const MapDisplay = ({refresh}) => { } }, [flightFollowing, planePosition.current]) + useEffect(async() => { + let data = await simConnectGetFlightPlan(); + + if (data !== undefined && data !== null) { + setWaypoints(data.waypoints); + } + }, [simConnectSystemEvent, refresh]) + + useEffect(() => { + if(waypoints != null && waypoints.length > 1) + { + layerGroupFlightPlan.current.clearLayers(); + + if(showFlightPlan) + drawFlightPath(waypoints, layerGroupFlightPlan.current, mapDefaults.planeRadiusCircleRange) + else + layerGroupFlightPlan.current.clearLayers(); + + } + }, [showFlightPlan, waypoints]) + return useMemo(() => ( @@ -271,6 +370,9 @@ const MapDisplay = ({refresh}) => { + + + ), [showFlightPlan, flightFollowing, layerGroupFlightPlan.current]) } diff --git a/ReactClient/src/Services/LocalStorageProvider.js b/ReactClient/src/Services/LocalStorageProvider.js index 4ec247b..27a80ee 100644 --- a/ReactClient/src/Services/LocalStorageProvider.js +++ b/ReactClient/src/Services/LocalStorageProvider.js @@ -14,8 +14,6 @@ const LocalStorageProvider = ({ initialData, children }) => { // Set default map config setMapConfig({ flightFollowing: true, - showFlightPlan: true, - showFlightPlanLabel: false, currentLayer: 'Bing Roads' }); diff --git a/ReactClient/src/Services/SimConnectDataProvider.js b/ReactClient/src/Services/SimConnectDataProvider.js index cd0b5a8..e0cceac 100644 --- a/ReactClient/src/Services/SimConnectDataProvider.js +++ b/ReactClient/src/Services/SimConnectDataProvider.js @@ -156,4 +156,16 @@ export const getLocalPopoutPanelDefinitions = async (panelRootPath, subPanelRoot console.error('Unable to retrieve pop out panel definitions. There may be an error with PopoutPanelDefinition.json.') return null; } +} + +export const simConnectGetFlightPlan = async () => { + try { + let response = await fetch(`${API_URL.url}/getflightplan`); + let result = await response.json(); + + return result; + } + catch { + console.error('MSFS unable to load flight plan.') + } } \ No newline at end of file diff --git a/SimconnectAgent/SimConnectProvider.cs b/SimconnectAgent/SimConnectProvider.cs index 0653a7b..398ebe0 100644 --- a/SimconnectAgent/SimConnectProvider.cs +++ b/SimconnectAgent/SimConnectProvider.cs @@ -1,5 +1,4 @@ -using MSFSPopoutPanelManager.Shared; -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; @@ -176,19 +175,19 @@ namespace MSFSPopoutPanelManager.SimConnectAgent private void HandleSimConnected(object source, EventArgs e) { - OnConnected?.Invoke(this, null); - // Start data request timer _requestDataTimer = new System.Timers.Timer(); _requestDataTimer.Interval = MSFS_DATA_REFRESH_TIMEOUT; _requestDataTimer.Enabled = true; _requestDataTimer.Elapsed += HandleDataRequested; _requestDataTimer.Elapsed += HandleMessageReceived; + + OnConnected?.Invoke(this, null); } private void HandleSimDisonnected(object source, EventArgs e) { - FileLogger.WriteLog($"MSFS is closed.", StatusMessageType.Info); + _requestDataTimer.Enabled = false; OnDisconnected?.Invoke(this, null); StopAndReconnect(); } diff --git a/SimconnectAgent/SimConnector.cs b/SimconnectAgent/SimConnector.cs index 83422aa..930d184 100644 --- a/SimconnectAgent/SimConnector.cs +++ b/SimconnectAgent/SimConnector.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; +using System.Threading.Tasks; using System.Timers; namespace MSFSPopoutPanelManager.SimConnectAgent @@ -15,6 +16,7 @@ namespace MSFSPopoutPanelManager.SimConnectAgent private SimConnect _simConnect; private Timer _connectionTimer; + private bool _isDisabledReconnect; public event EventHandler OnException; public event EventHandler> OnReceivedData; @@ -79,7 +81,8 @@ namespace MSFSPopoutPanelManager.SimConnectAgent foreach (var definition in SimConnectDataDefinitions) { - _simConnect.RequestDataOnSimObjectType(definition.RequestId, definition.DefineId, 0, SIMCONNECT_SIMOBJECT_TYPE.USER); + if (definition.DataDefinitionType == DataDefinitionType.SimConnect) + _simConnect.RequestDataOnSimObjectType(definition.RequestId, definition.DefineId, 0, SIMCONNECT_SIMOBJECT_TYPE.USER); } } @@ -90,14 +93,20 @@ namespace MSFSPopoutPanelManager.SimConnectAgent try { - _simConnect.ReceiveMessage(); + if (!_isDisabledReconnect) + _simConnect.ReceiveMessage(); } catch (Exception ex) { if (ex.Message != "0xC00000B0") { FileLogger.WriteLog($"SimConnector: SimConnect receive message exception - {ex.Message}", StatusMessageType.Error); + } + if (!_isDisabledReconnect) + { + // Prevent multiple reconnects from running + _isDisabledReconnect = true; // Need to stop and reconnect server since the data is SimConnect connection or data is probably corrupted. StopAndReconnect(); } @@ -167,16 +176,21 @@ namespace MSFSPopoutPanelManager.SimConnectAgent AddDataDefinitions(); - for (var i = 0; i < 5; i++) - { - System.Threading.Thread.Sleep(1000); - ReceiveMessage(); - } - _simConnect.RequestSystemState(SystemStateRequestId.AIRCRAFTPATH, "AircraftLoaded"); - Connected = true; + _isDisabledReconnect = false; + + Task.Run(() => + { + for (var i = 0; i < 5; i++) + { + System.Threading.Thread.Sleep(1000); + ReceiveMessage(); + } + }); + OnConnected?.Invoke(this, null); + Connected = true; StatusMessageWriter.WriteMessage("MSFS is connected", StatusMessageType.Info, false); } @@ -312,10 +326,16 @@ namespace MSFSPopoutPanelManager.SimConnectAgent private void SetActiveAircraftTitle(string aircraftFilePath) { var filePathToken = aircraftFilePath.Split(@"\"); - var aircraftName = filePathToken[filePathToken.Length - 2]; - aircraftName = aircraftName.Replace("_", " ").ToUpper(); - SimConnectDataDefinitions.Find(s => s.PropName == "AircraftName").Value = aircraftName; + if (filePathToken.Length > 1) + { + var aircraftName = filePathToken[filePathToken.Length - 2]; + aircraftName = aircraftName.Replace("_", " ").ToUpper(); + + SimConnectDataDefinitions.Find(s => s.PropName == "AircraftName").Value = aircraftName; + + OnReceivedData?.Invoke(this, SimConnectDataDefinitions); + } } } } \ No newline at end of file diff --git a/SimconnectAgent/SimconnectAgent.csproj b/SimconnectAgent/SimconnectAgent.csproj index f82eab2..dfa8ae4 100644 --- a/SimconnectAgent/SimconnectAgent.csproj +++ b/SimconnectAgent/SimconnectAgent.csproj @@ -32,6 +32,8 @@ + + diff --git a/SimconnectAgent/TouchPanel/DataProvider.cs b/SimconnectAgent/TouchPanel/DataProvider.cs index cc11fa4..97438db 100644 --- a/SimconnectAgent/TouchPanel/DataProvider.cs +++ b/SimconnectAgent/TouchPanel/DataProvider.cs @@ -3,6 +3,8 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; +using System.Dynamic; +using System.IO; using System.Timers; namespace MSFSPopoutPanelManager.SimConnectAgent.TouchPanel @@ -44,6 +46,29 @@ namespace MSFSPopoutPanelManager.SimConnectAgent.TouchPanel } } + public string GetFlightPlan() + { + // MSFS 2020 Windows Store version: C:\Users\{username}\AppData\Local\Packages\Microsoft.FlightSimulator_8wekyb3d8bbwe\LocalState\MISSIONS\Custom\CustomFlight\CustomFlight.FLT + // MSFS 2020 Steam version: C:\Users\{username}\AppData\Roaming\Microsoft Flight Simulator\MISSIONS\Custom\CustomFlight\CustomFlight.FLT + var filePathMSStore = Environment.ExpandEnvironmentVariables("%LocalAppData%") + @"\Packages\Microsoft.FlightSimulator_8wekyb3d8bbwe\LocalState\MISSIONS\Custom\CustomFlight\CustomFlight.FLT"; + var filePathSteam = Environment.ExpandEnvironmentVariables("%AppData%") + @"\Microsoft Flight Simulator\MISSIONS\Custom\CustomFlight\CustomFlight.FLT"; + + string filePath; + + if (File.Exists(filePathMSStore)) + filePath = filePathMSStore; + else if (File.Exists(filePathSteam)) + filePath = filePathSteam; + else + filePath = null; + + // cannot find CustomFlight.PLN, return empty set of waypoints + if (filePath == null) + return JsonConvert.SerializeObject(new List()); + + return FlightPlanProvider.ParseCustomFLT(filePath); + } + private void HandleDataRequested(object sender, ElapsedEventArgs e) { try diff --git a/SimconnectAgent/TouchPanel/FlightPlanProvider.cs b/SimconnectAgent/TouchPanel/FlightPlanProvider.cs new file mode 100644 index 0000000..287c04a --- /dev/null +++ b/SimconnectAgent/TouchPanel/FlightPlanProvider.cs @@ -0,0 +1,129 @@ +using CoordinateSharp; +using IniParser; +using IniParser.Model; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace MSFSPopoutPanelManager.SimConnectAgent.TouchPanel +{ + public class FlightPlanProvider + { + public static string ParseCustomFLT(string filePath) + { + var wayPoints = new List(); + + try + { + var parser = new FileIniDataParser(); + var data = parser.ReadFile(filePath); + + var isActiveFlightPlan = Convert.ToBoolean(data["ATC_Aircraft.0"]["ActiveFlightPlan"]); + var isRequestedFlightPlan = Convert.ToBoolean(data["ATC_Aircraft.0"]["RequestedFlightPlan"]); + + PropertyCollection flightPlan; + + // If FLT file has both requested and active flight plan set to true, requested flight plan takes precedence + if (isRequestedFlightPlan) + { + flightPlan = data["ATC_RequestedFlightPlan.0"]; + } + else if (isActiveFlightPlan) + { + flightPlan = data["ATC_ActiveFlightPlan.0"]; + } + else + { + return JsonConvert.SerializeObject(wayPoints); + } + + if (flightPlan != null) + { + int i = 0; + Coordinate c; + + while (true) + { + var waypointData = flightPlan["waypoint." + i]; + + if (waypointData == null) + break; + + var waypointArr = waypointData.Split(","); + + var waypoint = new ATCWaypoint(); + waypoint.index = i + 1; + waypoint.type = waypointArr[4].Trim(); + waypoint.description = waypointArr[3].Trim(); + + // exclude unnecessary user waypoints + if (!(waypoint.type == "V" || (waypoint.type == "U" && (waypoint.description == "TIMECLIMB" || waypoint.description == "TIMECRUIS" || waypoint.description == "TIMEDSCNT" || waypoint.description == "TIMEAPPROACH" || waypoint.description == "TIMEVERT")))) + { + waypoint.id = waypoint.type == "U" ? waypoint.description : waypointArr[1].Trim(); + + // parse "+003000.00" - 3000ft + var alt = Convert.ToInt32(waypointArr[17].Trim()); + waypoint.altitude = alt == 0 ? null : alt; + + var spd = Convert.ToInt32(waypointArr[16].Trim()); + waypoint.maxSpeed = spd == 0 ? null : spd; + + Coordinate.TryParse(waypointArr[5].Trim() + " " + waypointArr[6].Trim(), out c); + waypoint.latLong = new double[] { c.Latitude.DecimalDegree, c.Longitude.DecimalDegree }; + + //waypoint.departureProcedure = String.IsNullOrEmpty(waypointArr[9].Trim()) ? null : waypointArr[9].Trim(); + //waypoint.arrivalProcedure = String.IsNullOrEmpty(waypointArr[10].Trim()) ? null : waypointArr[10].Trim(); + //waypoint.approachType = String.IsNullOrEmpty(waypointArr[11].Trim()) ? null : waypointArr[11].Trim(); + //waypoint.approachRunway = String.IsNullOrEmpty(waypointArr[12].Trim()) ? null : waypointArr[12].Trim(); + + wayPoints.Add(waypoint); + } + + i++; + } + } + + return JsonConvert.SerializeObject(new FlightPlan() { waypoints = wayPoints, activeLegIndex = 0 }); + } + catch (Exception) + { + return JsonConvert.SerializeObject(wayPoints); + } + } + + public class FlightPlan + { + public int activeLegIndex { get; set; } + + public List waypoints { get; set; } + + public int dtk { get; set; } + } + + public class ATCWaypoint + { + public string id { get; set; } + + public string description { get; set; } + + public int index { get; set; } + + [JsonProperty("type")] + public string type { get; set; } + + public double[] latLong { get; set; } + + public double[] startLatLong { get; set; } + + public int? altitude { get; set; } + + public int? maxSpeed { get; set; } + + public double? distance { get; set; } + + public int? course { get; set; } + + public bool isActiveLeg { get; set; } + } + } +} diff --git a/SimconnectAgent/TouchPanel/TouchPanelSimConnectProvider.cs b/SimconnectAgent/TouchPanel/TouchPanelSimConnectProvider.cs index e3c41d9..8c44a4a 100644 --- a/SimconnectAgent/TouchPanel/TouchPanelSimConnectProvider.cs +++ b/SimconnectAgent/TouchPanel/TouchPanelSimConnectProvider.cs @@ -70,6 +70,12 @@ namespace MSFSPopoutPanelManager.SimConnectAgent.TouchPanel _simConnector.ResetSimConnectDataArea(planeId); } + public string GetFlightPlan() + { + return _dataProvider.GetFlightPlan(); + } + + private void InitializeProviders() { _dataProvider = new DataProvider(_simConnector); diff --git a/SimconnectAgent/TouchPanel/TouchPanelSimConnector.cs b/SimconnectAgent/TouchPanel/TouchPanelSimConnector.cs index 3c1d9ec..10a43b8 100644 --- a/SimconnectAgent/TouchPanel/TouchPanelSimConnector.cs +++ b/SimconnectAgent/TouchPanel/TouchPanelSimConnector.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Threading.Tasks; namespace MSFSPopoutPanelManager.SimConnectAgent.TouchPanel { @@ -147,7 +148,6 @@ namespace MSFSPopoutPanelManager.SimConnectAgent.TouchPanel private void InitializeSimConnect() { - // The constructor is similar to SimConnect_Open in the native API _simConnect = new SimConnect("TouchPanel Simconnect - Touch Panel Server", Process.GetCurrentProcess().MainWindowHandle, WM_USER_SIMCONNECT, null, 0); _connectionTimer.Enabled = false; @@ -167,11 +167,28 @@ namespace MSFSPopoutPanelManager.SimConnectAgent.TouchPanel _simConnect.UnsubscribeFromSystemEvent(SimConnectSystemEvent.VIEW); _simConnect.SubscribeToSystemEvent(SimConnectSystemEvent.VIEW, "View"); - for (var i = 0; i < 5; i++) + Task.Run(() => { - System.Threading.Thread.Sleep(1000); - ReceiveMessage(); - } + for (var i = 0; i < 5; i++) + { + System.Threading.Thread.Sleep(1000); + ReceiveMessage(); + } + }); + + MobilFlightInitialize(); + + if (_planeId != null) + ResetSimConnectDataArea(_planeId); + + // MobiFlight wasm event + _simConnect.OnRecvClientData -= HandleOnRecvClientData; + _simConnect.OnRecvClientData += HandleOnRecvClientData; + + Connected = true; + + OnConnected?.Invoke(this, null); + MobiFlightWasmClient.Ping(_simConnect); } private void AddDataDefinitions() @@ -215,19 +232,7 @@ namespace MSFSPopoutPanelManager.SimConnectAgent.TouchPanel private void HandleOnRecvOpen(SimConnect sender, SIMCONNECT_RECV_OPEN data) { - MobilFlightInitialize(); - - if (_planeId != null) - ResetSimConnectDataArea(_planeId); - - // MobiFlight wasm event - _simConnect.OnRecvClientData -= HandleOnRecvClientData; - _simConnect.OnRecvClientData += HandleOnRecvClientData; - - Connected = true; - - OnConnected?.Invoke(this, null); - MobiFlightWasmClient.Ping(_simConnect); + ReceiveMessage(); } private void HandleOnRecvQuit(SimConnect sender, SIMCONNECT_RECV data) diff --git a/UserDataAgent/AppSetting.cs b/UserDataAgent/AppSetting.cs index fad5d42..8c011a5 100644 --- a/UserDataAgent/AppSetting.cs +++ b/UserDataAgent/AppSetting.cs @@ -152,7 +152,7 @@ namespace MSFSPopoutPanelManager.UserDataAgent // Default values EnableTouchPanelIntegration = false; DataRefreshInterval = 200; - MapRefreshInterval = 250; + MapRefreshInterval = 1000; UseArduino = false; EnableSound = true; } diff --git a/WebServer/Controllers/DataController.cs b/WebServer/Controllers/DataController.cs index 5096611..84aa800 100644 --- a/WebServer/Controllers/DataController.cs +++ b/WebServer/Controllers/DataController.cs @@ -43,7 +43,7 @@ namespace MSFSPopoutPanelManager.WebServer.Controllers } catch { - return new SimConnectData { Data = null, MsfsStatus = false, ArduinoStatus = false, SystemEvent = null, G1000NxiFlightPlan = null }; + return new SimConnectData { Data = null, MsfsStatus = false, ArduinoStatus = false, SystemEvent = null }; } } @@ -99,6 +99,12 @@ namespace MSFSPopoutPanelManager.WebServer.Controllers return new TouchPanelConfigSetting(); } + + [HttpGet("/getflightplan")] + public string GetFlightPlan() + { + return _simConnectService.GetFlightPlan(); + } } public class SimConnectData @@ -110,8 +116,6 @@ namespace MSFSPopoutPanelManager.WebServer.Controllers public bool ArduinoStatus { get; set; } public string SystemEvent { get; set; } - - public string G1000NxiFlightPlan { get; set; } } public class TouchPanelLoadedPostData diff --git a/WebServer/SimConnectService.cs b/WebServer/SimConnectService.cs index cc8a57b..0014bb3 100644 --- a/WebServer/SimConnectService.cs +++ b/WebServer/SimConnectService.cs @@ -73,6 +73,11 @@ namespace MSFSPopoutPanelManager.WebServer { _simConnectorProvider.ResetSimConnectDataArea(planeId); } + + public string GetFlightPlan() + { + return _simConnectorProvider.GetFlightPlan(); + } } public interface ISimConnectService @@ -87,6 +92,8 @@ namespace MSFSPopoutPanelManager.WebServer public void ResetSimConnectDataArea(string planeId); + public string GetFlightPlan(); + public TouchPanelConfigSetting TouchPanelConfigSetting { get; set; } } } diff --git a/WindowsAgent/WindowActionManager.cs b/WindowsAgent/WindowActionManager.cs index aba5f84..66e8a7f 100644 --- a/WindowsAgent/WindowActionManager.cs +++ b/WindowsAgent/WindowActionManager.cs @@ -24,10 +24,6 @@ namespace MSFSPopoutPanelManager.WindowsAgent public static void ApplyAlwaysOnTop(IntPtr hwnd, PanelType panelType, bool alwaysOnTop, Rectangle panelRectangle) { - // Override weird size adjustment for Touch Panel WPF window - int newWidth = panelType == PanelType.MSFSTouchPanel ? panelRectangle.Width - 16 : panelRectangle.Width; - int newHeight = panelType == PanelType.MSFSTouchPanel ? panelRectangle.Height - 39 : panelRectangle.Height; - if (panelType == PanelType.PopOutManager) { OnPopOutManagerAlwaysOnTopChanged?.Invoke(null, alwaysOnTop); @@ -35,9 +31,9 @@ namespace MSFSPopoutPanelManager.WindowsAgent else { if (alwaysOnTop) - PInvoke.SetWindowPos(hwnd, new IntPtr(PInvokeConstant.HWND_TOPMOST), panelRectangle.Left, panelRectangle.Top, newWidth, newHeight, PInvokeConstant.SWP_ALWAYS_ON_TOP); + PInvoke.SetWindowPos(hwnd, new IntPtr(PInvokeConstant.HWND_TOPMOST), panelRectangle.Left, panelRectangle.Top, panelRectangle.Width, panelRectangle.Height, PInvokeConstant.SWP_ALWAYS_ON_TOP); else - PInvoke.SetWindowPos(hwnd, new IntPtr(PInvokeConstant.HWND_NOTOPMOST), panelRectangle.Left, panelRectangle.Top, newWidth, newHeight, 0); + PInvoke.SetWindowPos(hwnd, new IntPtr(PInvokeConstant.HWND_NOTOPMOST), panelRectangle.Left, panelRectangle.Top, panelRectangle.Width, panelRectangle.Height, 0); } } @@ -64,24 +60,16 @@ namespace MSFSPopoutPanelManager.WindowsAgent PInvoke.MoveWindow(hwnd, x, y, rectangle.Width, rectangle.Height, true); } - public static void MoveWindow(IntPtr hwnd, PanelType panelType, int x, int y, int width, int height) + public static void MoveWindow(IntPtr hwnd, int x, int y, int width, int height) { - // Override weird size adjustment for Touch Panel WPF window - int newWidth = panelType == PanelType.MSFSTouchPanel ? width - 16 : width; - int newHeight = panelType == PanelType.MSFSTouchPanel ? height - 39 : height; - - PInvoke.MoveWindow(hwnd, x, y, newWidth, newHeight, true); + PInvoke.MoveWindow(hwnd, x, y, width, height, true); } - public static void MoveWindowWithMsfsBugOverrirde(IntPtr hwnd, PanelType panelType, int x, int y, int width, int height) + public static void MoveWindowWithMsfsBugOverrirde(IntPtr hwnd, int x, int y, int width, int height) { int originalX = x; - // Override weird size adjustment for Touch Panel WPF window - int newWidth = panelType == PanelType.MSFSTouchPanel ? width - 16 : width; - int newHeight = panelType == PanelType.MSFSTouchPanel ? height - 39 : height; - - PInvoke.MoveWindow(hwnd, x, y, newWidth, newHeight, true); + PInvoke.MoveWindow(hwnd, x, y, width, height, true); // 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 @@ -91,7 +79,7 @@ namespace MSFSPopoutPanelManager.WindowsAgent PInvoke.GetWindowRect(hwnd, out rectangle); if (rectangle.Left != originalX) - PInvoke.MoveWindow(hwnd, originalX, y, newWidth, newHeight, false); + PInvoke.MoveWindow(hwnd, originalX, y, width, height, false); } public static void MinimizeWindow(IntPtr hwnd) diff --git a/WpfApp/OnScreenMessageDialog.xaml.cs b/WpfApp/OnScreenMessageDialog.xaml.cs index ada228b..b77c1ef 100644 --- a/WpfApp/OnScreenMessageDialog.xaml.cs +++ b/WpfApp/OnScreenMessageDialog.xaml.cs @@ -43,7 +43,7 @@ namespace MSFSPopoutPanelManager.WpfApp var x = Convert.ToInt32(rectangle.X + clientRectangle.Width / 2 - this.Width / 2); var y = Convert.ToInt32(rectangle.Y + clientRectangle.Height / 2 - this.Height / 2); - WindowActionManager.MoveWindow(dialogHandle, PanelType.WPFWindow, x, y, Convert.ToInt32(this.Width), Convert.ToInt32(this.Height)); + WindowActionManager.MoveWindow(dialogHandle, x, y, Convert.ToInt32(this.Width), Convert.ToInt32(this.Height)); } else { diff --git a/WpfApp/PanelCoorOverlay.xaml.cs b/WpfApp/PanelCoorOverlay.xaml.cs index b8d45d9..5590291 100644 --- a/WpfApp/PanelCoorOverlay.xaml.cs +++ b/WpfApp/PanelCoorOverlay.xaml.cs @@ -56,7 +56,7 @@ namespace MSFSPopoutPanelManager.WpfApp { // Fixed broken window left/top coordinate for DPI Awareness Per Monitor var handle = new WindowInteropHelper(this).Handle; - WindowActionManager.MoveWindow(handle, PanelType.WPFWindow, _xCoor, _yCoor, Convert.ToInt32(this.Width), Convert.ToInt32(this.Height)); + WindowActionManager.MoveWindow(handle, _xCoor, _yCoor, Convert.ToInt32(this.Width), Convert.ToInt32(this.Height)); WindowActionManager.ApplyAlwaysOnTop(handle, PanelType.WPFWindow, true); } diff --git a/WpfApp/PreferencesDialog.xaml b/WpfApp/PreferencesDialog.xaml index f59721b..9edc61d 100644 --- a/WpfApp/PreferencesDialog.xaml +++ b/WpfApp/PreferencesDialog.xaml @@ -260,7 +260,7 @@ - Amount of time to delay touch down and then touch up event when operating touch enabled panel. If your touch is not registering consistently, increasing this value will help. + Amount of time in milliseconds to delay touch down and then touch up event when operating touch enabled panel. If your touch is not registering consistently, increasing this value will help. For panel display on direct connected touch monitor, 0 milliseconds work really well. @@ -310,8 +310,8 @@ Map Refresh Interval - - Time interval for touch panel's map to refresh SimConnect data. (Default: 250 miliseconds) + + Time interval for touch panel's map to refresh SimConnect data. (Default: 1000 miliseconds) diff --git a/WpfApp/Resources/info_icon.png b/WpfApp/Resources/info_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9abb561878335716bf92cb35d8fdc0ae6fded313 GIT binary patch literal 2150 zcmbVO2~ZPP7+$;SeoFtY;CdI;D7{!Uz>DjA#|bYBvEgosO-oJM;Fvx9@x3fB&<#=*VgP z-F)2u0O%i~R>g3$kA3v*!@ciKsM*C${V4Su1_0c>?861vx_vMJ^lBwEacms?xdJ6k z0t6$qxWHR!2=0Q$(t>Og)kL{8whnOjmM@&YS7d~ zRF3gNCWFCN1xH}QSp>A2jAlk*Rq`CX3T|!x4Dmn*giTcPLhS;@%b?Tm<)Ok$NhPS;K& zOq7$>%yc=5a~WhsC`cp_+KqAmG1Q5pl4zqtIEF&F5jWvxmf^4>CzkUQ3_IcPq&1nG zXod|><}~WE?VV^wlS1K849<{AG>V5Oa}q~(D@GYemSha%KiJ_sc3MLz!e|^}Nm@ga z#_mu>ce8+Eu|Nurn@yN8(!z|lOX<3Rs}L4f^6XLL3n%eK0UDtrY7!}57@!aeJE1U_ z2^hj6{|!uHgf8X1Pz+V*NZN#OULs709)~Ei-XRT!6%l5JMa(E3p;GcVRRsirDFWqE znNA|n@eve}@AtTb(Ik5WyYFNF^L`Xfa5aP&Kb*%I zAw?KzBx#t$I4?`=`Rm9dg_>ZvV5M|bL@d6jYh@%rN9rgL)Slf+9%>IOj`6xa6CdoE zPFjlr=Scr0+?_Cn)Ug(X#zXa-7vGys5T`t3uft9|pud~wIQvV@cEhe&}Ft)P2_F{9?l>LKF)wtE&Y1%QoVEgtV1?wW$&N^6E<#N6KkJ5A1XI4FZ zS@P`mHfa&GYgoERuk#mA1XqMURds`UUXS`PW8DTYn&Ez?J@u*J|T7@Q=QEG;;n%L(#`C+kZZoBj_-C zwQLT%pMTYJvNsz$C7lmXICkpQ$h1ir^J#Z9S8xdsxa9ffuZt{&ftDXaSB|MWV`(nV z%)a;1vNwF2X}Y&+MvaecaG%%upXx)H+AFRJ1ABd$sn!(FFozf4_^79}~6AlTCmRDJqgYVqW!+$rrt>vd2d7RHNQz; zFfO=G?WZ}ovau2Bm^w3PxsN*$8nkb8VC;;Qjj3pHa?+60ohGuVd~0OiDZySFhxXY} z*mkF)NKHA(H`*WG^tt~ztv)ir5;_SDSymfH(zJQm8r-#kiq zn(rF2q^9A8dz)b&uJ%~5DeO=ptc0@L8_LS&!iTDR?$L$h`zzB@{Y#E!Bu6K@8}`m? ziz*nERhl8UmhXgSKTp21_|c=L>RJAW)8j8(xns=P<9Aa3pl;(`KxQr4DH}8J?c>Lr z;^C;8f`eo9@qRghd%rnMU=5z?bK0A=7vtk)r-oiitqSta>#ZABQ1@`DsCTO^i(OoP zeY9=iiFI=)9?hyO%%5IZQutlpYFTOh(P#GnL4w=r9&Q8rV4&yjrM(8YvgF$(F1HFj Vwv5OWei`x7D7FE{`I literal 0 HcmV?d00001 diff --git a/WpfApp/UserControlPanelConfiguration.xaml b/WpfApp/UserControlPanelConfiguration.xaml index 331db7d..3c774fe 100644 --- a/WpfApp/UserControlPanelConfiguration.xaml +++ b/WpfApp/UserControlPanelConfiguration.xaml @@ -35,12 +35,16 @@ -