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

Merge v3.4 branch

This commit is contained in:
Stanley 2022-07-23 15:23:32 -04:00
parent 3a54c859f5
commit e49fe68227
457 changed files with 73601 additions and 3646 deletions

3
Addons/InGameToolbar/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
_Temp/
Packages/
PackagesMetadata/

Binary file not shown.

View file

@ -0,0 +1,30 @@
<AssetPackage Name="pop-out-manager" Version="1.0.0">
<ItemSettings>
<ContentType>SPB</ContentType>
<Title>pop-out-manager</Title>
<Manufacturer>Stanley Kwok</Manufacturer>
<Creator>Stanley Kwok</Creator>
</ItemSettings>
<Flags>
<VisibleInStore>false</VisibleInStore>
<CanBeReferenced>false</CanBeReferenced>
</Flags>
<AssetGroups>
<AssetGroup Name="pop-out-manager">
<Type>SPB</Type>
<Flags>
<FSXCompatibility>false</FSXCompatibility>
</Flags>
<AssetDir>PackageSources\pop-out-manager\</AssetDir>
<OutputDir>InGamePanels\</OutputDir>
</AssetGroup>
<AssetGroup Name="html-ui">
<Type>Copy</Type>
<Flags>
<FSXCompatibility>false</FSXCompatibility>
</Flags>
<AssetDir>PackageSources\html_ui\</AssetDir>
<OutputDir>html_ui\</OutputDir>
</AssetGroup>
</AssetGroups>
</AssetPackage>

View file

@ -0,0 +1,138 @@
body.modal-open {
pointer-events: none;
}
body.modal-open .stepDialog {
pointer-events: auto;
}
pop-out-manager #popOutManager .ingameUiWrapper .horizontalLayout {
background-color: var(--backgroundColorPanel);
display: flex;
flex-direction: column;
padding: 1em 1em;
}
pop-out-manager #popOutManager .ingameUiWrapper .horizontalLayout #profileInfo {
flex: 1 1 0;
margin-bottom: 1em;
}
pop-out-manager #popOutManager .ingameUiWrapper .horizontalLayout #panelConfigTable {
display: flex;
flex-direction: column;
border: solid 1px white;
flex: 20 1 0;
}
pop-out-manager #popOutManager .ingameUiWrapper .horizontalLayout #panelConfigButtons {
display: flex;
flex-direction: row;
position: relative;
margin: 2em 1em 1em 1em;
flex: 2 1 0;
}
pop-out-manager #popOutManager .ingameUiWrapper .panelConfigButton {
display: flex;
justify-content: center;
width: 6em;
height: 3em;
margin-right: 1em;
}
pop-out-manager #popOutManager .ingameUiWrapper .lockPanelsButton {
display: flex;
justify-content: center;
position: absolute;
right: 0;
width: 10em;
height: 3em;
}
.panelRow {
display: flex;
margin: 0;
padding: 0;
}
.panelRow .column1 {
width: 34%;
}
.panelRow .column2 {
width: 8%;
}
.panelRow .column3 {
width: 8%;
}
.panelRow .column4 {
width: 8%;
}
.panelRow .column5 {
width: 8%;
}
.panelRow .column6 {
width: 8%;
}
.panelRow .column7 {
width: 8%;
}
.panelRow .column8 {
width: 10%;
}
.panelRow .column9 {
width: 8%;
}
.panelCell {
display: flex;
padding: 0.7em 1em;
margin: 0;
justify-content: center;
text-align: center;
border-bottom: solid 1px white;
border-right: solid 1px white;
}
.panelCell:last-child {
border-right: none;
}
.panelCell .alignCenter {
display: flex;
justify-content: center;
align-items: center;
}
.panelCell ui-input .default-input input {
padding-left: 0.25em;
padding-right: 0.25em;
}
.panelCell checkbox-element .checkbox .choices .iconWrapper .icon {
width: 1.6em;
height: 1.6em;
}
.stepDialog {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
left: 20vw;
top: 20vh;
width: 60vw; /* Full width */
height: 60vh; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgba(161, 153, 153, 1);
}

View file

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="/SCSS/common.css" />
<link rel="stylesheet" href="PopOutManager.css" />
<script type="text/javascript" src="/JS/coherent.js"></script>
<script type="text/javascript" src="/JS/common.js"></script>
<script type="text/javascript" src="/JS/buttons.js"></script>
<script type="text/javascript" src="/JS/Services/ToolBarPanels.js"></script>
<script type="text/javascript" src="/JS/Services/Notifications.js"></script>
<link rel="import" href="/templates/virtualScroll/virtualScroll.html" />
<link rel="import" href="/templates/NewPushButton/NewPushButton.html" />
<link rel="import" href="/templates/DropDown/DropDown.html" />
<link rel="import" href="/templates/tabMenu/tabMenu.html" />
<link rel="import" href="/templates/ingameUi/ingameUi.html" />
<link rel="import" href="/templates/ingameUiHeader/ingameUiHeader.html" />
<link rel="import" href="/templates/checkbox/checkbox.html" />
<link rel="import" href="/templates/uiInput/uiInput.html" />
<script type="text/javascript" src="PopOutManager.js"></script>
</head>
<body class="border-box">
<pop-out-manager>
<ingame-ui id="popOutManager" panel-id="PANEL_POP_OUT_MANAGER" title=""
class="ingameUiFrame panelInvisible condensedPanel" resize="both" min-width="160" min-height="90"
content-fit="true" auto-inside>
<div id="panelSelection" class="horizontalLayout" style="display:block">
<div id="profileInfo">Panel Locations and Settings - <span id="planeProfileName"></span></div>
<div id="panelSelection">
Panel Selection
<drop-down id="dropdownProfile" style="width:50vw"></drop-down>
<icon-button id="addProfile" data-url="/icons/open.svg"></icon-button>
<icon-button id="deleteProfile" data-url="/icons/reduce.svg"></icon-button>
</div>
</div>
<div id="panelConfiguration" class="horizontalLayout" style="display:none">
<div id="profileInfo">Panel Locations and Settings - <span id="planeProfileName"></span></div>
<virtual-scroll id="panelConfigTable" direction="y">
</virtual-scroll>
<div id="panelConfigButtons">
<new-push-button id="btnMinusTen" title="-10 px" class="panelConfigButton"></new-push-button>
<new-push-button id="btnMinusOne" title="-1 px" class="panelConfigButton"></new-push-button>
<new-push-button id="btnPlusOne" title="+1 px" class="panelConfigButton"></new-push-button>
<new-push-button id="btnPlusTen" title="+10 px" class="panelConfigButton" toolTip="test"></new-push-button>
<div>
<new-push-button id="btnLockPanels" title="Lock Panels" class="lockPanelsButton"></new-push-button>
</div>
</div>
</div>
</ingame-ui>
</pop-out-manager>
<div id="dialogBegin" class="stepDialog">
<new-push-button id="btnStartPopOut" parentStep="stepBegin" title="Start Pop Out" class="stepButton"></new-push-button>
<new-push-button id="btnCreateNewProfile" parentStep="stepBegin" title="Create New Profile" class="stepButton"></new-push-button>
<new-push-button id="btnAdjustProfile" parentStep="stepBegin" title="Adjust Profile" class="stepButton"></new-push-button>
</div>
</body>
</html>

View file

@ -0,0 +1,388 @@
class PopOutManagerPanelElement extends UIElement {
constructor() {
super(...arguments);
this.ingameUi = null;
this.isInitialized = false;
this.panelActive = false;
this.webSocket = null;
this.webSocketConnected = false;
this.webSocketInterval = null;
this.tryConnectWebSocket = () => {
this.lockPanel(true);
this.webSocket = new WebSocket("ws://localhost:27011/ws");
this.webSocket.onopen = () => {
clearInterval(this.webSocketInterval);
this.webSocketConnected = true;
};
this.webSocket.onclose = () => {
this.webSocketConnected = false;
// Clear panel table
this.createPanelConfigTableHeader(this.panelConfigTable);
this.lockPanel(true);
this.webSocketInterval = setInterval(() => {
if (!this.webSocketConnected)
this.tryConnectWebSocket();
}, 2000)
};
this.webSocket.onerror = () => {
this.webSocket.close();
};
this.webSocket.onmessage = (event) => {
if (event.data !== undefined) {
var panelData = JSON.parse(event.data);
// only recreate panel rows if panel is refreshed (minimize/maximize, pop out)
if (this.panelActive && document.getElementsByClassName("panelRow").length == 1)
this.createPanelConfigTableBody(this.panelConfigTable, panelData);
if (panelData !== undefined && panelData !== null && panelData.length !== 0) {
this.lockPanel(false);
this.bindPanelData(panelData);
}
}
};
}
this.webSocketInterval = setInterval(() => {
if (!this.webSocketConnected)
this.tryConnectWebSocket();
}, 2000)
}
connectedCallback() {
super.connectedCallback();
this.ingameUi = this.querySelector('ingame-ui');
this.panelSelection = document.getElementById("panelSelection");
this.panelConfiguration = document.getElementById("panelConfiguration");
this.planeProfileName = document.getElementById("planeProfileName");
this.panelConfigTable = document.getElementById("panelConfigTable");
this.btnLockPanels = this.querySelector('#btnLockPanels');
this.btnPlusTen = this.querySelector('#btnPlusTen');
this.btnPlusOne = this.querySelector('#btnPlusOne');
this.btnMinusTen = this.querySelector('#btnMinusTen');
this.btnMinusOne = this.querySelector('#btnMinusOne');
this.dropdownProfile = this.querySelector("#dropdownProfile");
this.addProfile = document.getElementById("addProfile");
this.deleteProfile = document.getElementById("deleteProfile");
this.stepBeginDialog = document.getElementById("stepBeginDialog");
this.closeDialog = document.getElementById("closeDialog");
//this.deleteProfile.disable(!this.deleteProfile.disabled);
//this.dropdownProfile.addEventListener("select", (event) = {});
let workflow = new Workflow(this);
workflow.bindAllButtonEvents();
workflow.stepBegin();
this.addProfile.addEventListener("click", (event) => {
addProfileDialog.style.display = "block";
//this.ingameUi.toggleExternPanel(true);
document.body.classList.toggle('modal-open');
// let value = new DataValue;
// value.name = "New Profile " + dropDownValues.length;
// value.ID = dropDownValues.length;
// dropDownValues.push(value);
// this.dropdownProfile.setData(dropDownValues, dropDownValues.length - 1);
});
this.deleteProfile.addEventListener("click", (event) => {
// dropDownValues.splice(-1,1);
// this.dropdownProfile.setData(dropDownValues, dropDownValues.length - 1);
});
this.isLocked = false;
let profiles = ["Kodiak", "172", "737"];
let dropDownValues = [];
profiles.forEach((profile, index) => {
let value = new DataValue;
value.name = profile;
value.ID = index;
dropDownValues.push(value);
});
setTimeout(() => {
this.dropdownProfile.setData(dropDownValues, 2)
}, 1000);
this.btnLockPanels.addEventListener("click", () => {
if (this.btnLockPanels.disabled)
return;
this.isLocked = !this.isLocked;
this.lockPanel(this.isLocked);
if (this.isLocked) {
this.btnLockPanels.disabled = false;
this.btnLockPanels.title = "Unlock Panels";
this.btnLockPanels.style.backgroundColor = "red";
}
else {
this.btnLockPanels.disabled = false;
this.btnLockPanels.title = "Lock Panels";
this.btnLockPanels.style.backgroundColor = null;
}
this.panelSelection.style.display = 'block';
this.panelConfiguration.style.display = 'none';
});
if (this.ingameUi) {
this.ingameUi.addEventListener("panelActive", (e) => {
//document.getElementsByClassName("Extern")[0].style.display = "none"; // disable extern button
this.createPanelConfigTableHeader(this.panelConfigTable);
this.panelActive = true;
this.lockPanel(true);
if (this.webSocketConnected)
this.webSocket.send("RequestPanelData");
});
}
}
createPanelConfigTableHeader(panelConfigTable) {
let panelRow;
// remove all child of panelConfigTable
while (panelConfigTable.firstChild) {
panelConfigTable.removeChild(panelConfigTable.firstChild);
}
// create header
panelRow = this.createPanelRow(true);
panelRow.appendChild(this.createPanelCell("div", "column1", "Panel Name"));
panelRow.appendChild(this.createPanelCell("div", "column2", "X-Pos"));
panelRow.appendChild(this.createPanelCell("div", "column3", "Y-Pos"));
panelRow.appendChild(this.createPanelCell("div", "column4", "Width"));
panelRow.appendChild(this.createPanelCell("div", "column5", "Height"));
panelRow.appendChild(this.createPanelCell("div", "column6", "Always on Top"));
panelRow.appendChild(this.createPanelCell("div", "column7", "Hide Title Bar"));
panelRow.appendChild(this.createPanelCell("div", "column8", "Full Screen Mode"));
panelRow.appendChild(this.createPanelCell("div", "column9", "Touch Enabled"));
panelConfigTable.appendChild(panelRow);
}
createPanelConfigTableBody(panelConfigTable, panelData) {
let panelRow;
if (panelConfigTable !== undefined) {
for (let index = 0; index < panelData.length; index++) {
panelRow = this.createPanelRow(false);
panelRow.appendChild(this.createPanelCell("div", "column1", this.createUiInput("PanelName_" + index)));
panelRow.appendChild(this.createPanelCell("div", "column2", this.createUiInput("XPos_" + index)));
panelRow.appendChild(this.createPanelCell("div", "column3", this.createUiInput("YPos_" + index)));
panelRow.appendChild(this.createPanelCell("div", "column4", this.createUiInput("Width_" + index)));
panelRow.appendChild(this.createPanelCell("div", "column5", this.createUiInput("Height_" + index)));
panelRow.appendChild(this.createPanelCell("div", "column6", this.createCheckbox("AlwaysOnTop_" + index)));
panelRow.appendChild(this.createPanelCell("div", "column7", this.createCheckbox("HideTitleBar_" + index)));
panelRow.appendChild(this.createPanelCell("div", "column8", this.createCheckbox("FullScreenMode_" + index)));
panelRow.appendChild(this.createPanelCell("div", "column9", this.createCheckbox("TouchEnabled_" + index)));
panelConfigTable.appendChild(panelRow);
}
}
}
createPanelRow(isHeaderRow) {
let panelRow = document.createElement("div");
if (isHeaderRow)
panelRow.classList.add("panelHeaderRow");
panelRow.classList.add("panelRow");
return panelRow;
}
createPanelCell(cellType, classes = null, childElement) {
let cell = document.createElement(cellType);
cell.classList.add("panelCell");
if (classes !== undefined || classes !== null) {
if (typeof (classes) === "string") {
cell.classList.add(classes);
}
else {
for (let index = 0; index < classes.length; index++)
cell.classList.add(classes[index]);
}
}
if (typeof (childElement) === "string")
cell.innerHTML = `<div class='alignCenter'>${childElement}</div>`;
else
cell.appendChild(childElement);
return cell;
}
createUiInput(id) {
let input = document.createElement("ui-input");
input.type = "text";
input.id = id;
return input;
}
createCheckbox(id) {
let checkbox = document.createElement("checkbox-element");
checkbox.id = id;
checkbox.style.width = "1em";
return checkbox;
}
lockPanel(isLocked) {
if (this.panelActive) {
let uiInputs = document.getElementsByTagName("ui-input");
for (let index = 0; index < uiInputs.length; index++) {
uiInputs[index].disabled = isLocked;
}
let checkboxes = document.getElementsByTagName("checkbox-element");
for (let index = 0; index < checkboxes.length; index++) {
checkboxes[index].SetData({ sTitle: "", bToggled: checkboxes.toggled, bDisabled: isLocked });
checkboxes[index].RefreshValue();
}
this.btnLockPanels.disabled = isLocked;
if(isLocked)
{
this.btnLockPanels.title = "Lock Panels";
this.btnLockPanels.style.backgroundColor = null;
}
this.btnPlusTen.disabled = isLocked;
this.btnPlusOne.disabled = isLocked;
this.btnMinusTen.disabled = isLocked;
this.btnMinusOne.disabled = isLocked;
}
}
bindPanelData(panelData) {
if (this.panelActive) {
panelData.forEach((panel, index) => {
this.bindUiInput("PanelName_" + index, panel.panelName, "text");
this.bindUiInput("XPos_" + index, panel.xPos);
this.bindUiInput("YPos_" + index, panel.yPos);
this.bindUiInput("Width_" + index, panel.width);
this.bindUiInput("Height_" + index, panel.height);
this.bindCheckbox("AlwaysOnTop_" + index, panel.alwaysOnTop);
this.bindCheckbox("HideTitleBar_" + index, panel.hideTitleBar);
this.bindCheckbox("FullScreenMode_" + index, panel.fullScreenMode);
this.bindCheckbox("TouchEnabled_" + index, panel.touchEnabled);
})
}
}
bindUiInput(id, value, type) {
let input = document.getElementById(id);
input.style.width = "100%";
input.setValue(value);
if (type === "text")
input.children[0].children[0].style.textAlign = "left";
}
bindCheckbox(id, value) {
let checkbox = document.getElementById(id);
checkbox.SetData({ sTitle: "", bToggled: value, bDisabled: false });
checkbox.RefreshValue();
}
}
window.customElements.define("pop-out-manager", PopOutManagerPanelElement);
class Workflow {
constructor(owner) {
this.owner = owner;
}
get workflowSteps() {
return [
{
name: 'stepBegin',
results:
[
{ value: 'stepCreateNewProfile' },
{ value: 'stepStartPopOut' },
{ value: 'stepAdjustProfile' }
]
}]
}
getFuncName() {
return this.getFuncName.caller.name;
}
bindAllButtonEvents(){
let stepButtons = document.getElementsByClassName("stepButton");
Array.from(stepButtons).forEach(btn => {
btn.addEventListener("click", (e) => {
let parentStep = e.target.getAttribute("parentStep");
let resultValue = e.target.id.replace("btn", "step");
this.handleButtonClick(parentStep, resultValue);
})
})
}
handleButtonClick(currentStep, resultValue) {
let step = this.workflowSteps.find(c => c.name == currentStep);
let result = step.results.find(f => f.value == resultValue);
if(result != null)
{
let func = this[result.value];
func();
}
}
openDialog(step)
{
this.owner.stepBeginDialog = document.getElementById(step.replace("step", "dialog"));
this.owner.stepBeginDialog.style.display = "block";
document.body.classList.toggle("modal-open");
}
stepBegin() {
this.openDialog("stepBegin");
}
stepStartPopOut() {
var a = ""
}
stepCreateNewProfile() {
var a = ""
}
stepAdjustProfile() {
var a = ""
}
}

View file

@ -0,0 +1,16 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="64.000000pt" height="64.000000pt" viewBox="0 0 64.000000 64.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,64.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M160 315 l0 -165 160 0 160 0 0 50 c0 43 -3 50 -20 50 -16 0 -20 -7
-20 -30 l0 -30 -120 0 -120 0 0 125 0 125 30 0 c23 0 30 4 30 20 0 17 -7 20
-50 20 l-50 0 0 -165z"/>
<path d="M358 447 l32 -33 -50 -49 c-47 -46 -49 -50 -33 -67 16 -18 19 -17 68
32 l51 50 27 -27 27 -28 0 78 0 77 -77 0 -77 0 32 -33z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 740 B

View file

@ -0,0 +1,29 @@
{
"content": [
{
"path": "html_ui/icons/toolbar/POP_OUT_MANAGER.svg",
"size": 740,
"date": 133013771715368552
},
{
"path": "html_ui/InGamePanels/PopOutManagerPanel/PopOutManager.css",
"size": 1921,
"date": 133015501657300878
},
{
"path": "html_ui/InGamePanels/PopOutManagerPanel/PopOutManager.html",
"size": 3194,
"date": 133023923647110813
},
{
"path": "html_ui/InGamePanels/PopOutManagerPanel/PopOutManager.js",
"size": 10854,
"date": 133023924937932717
},
{
"path": "InGamePanels/pop-out-manager.spb",
"size": 684,
"date": 133023927549810418
}
]
}

View file

@ -0,0 +1,15 @@
{
"dependencies": [],
"content_type": "UNKNOWN",
"title": "pop-out-manager",
"manufacturer": "Stanley Kwok",
"creator": "Stanley Kwok",
"package_version": "1.0.0",
"minimum_game_version": "1.27.9",
"release_notes": {
"neutral": {
"LastUpdate": "",
"OlderHistory": ""
}
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="Windows-1252"?>
<SimBase.Document Type="InGamePanels" version="1,0">
<Filename>InGamePanel_PopOutManager.spb</Filename>
<InGamePanels.InGamePanelDefinition id="PANEL_POP_OUT_MANAGER" Name="Pop Out Manager" url="html_UI/ingamePanels/PopOutManagerPanel/PopOutManager.html" resizeDirections="Both" minWidth="80" minHeight="40" defaultWidth="120" defaultHeight="50" defaultTop="30" defaultLeft="5" icon="POP_OUT_MANAGER" buttonVisible="true">
</InGamePanels.InGamePanelDefinition>
</SimBase.Document>

View file

@ -0,0 +1 @@
FOR %%i IN (*.xml) DO "%MSFS_SDK%\Tools\bin\fspackagetool.exe" %%i

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,7 @@
<Project Version="2" Name="pop-out-manager" FolderName="Packages">
<OutputDirectory>.</OutputDirectory>
<TemporaryOutputDirectory>_Temp</TemporaryOutputDirectory>
<Packages>
<Package>PackageDefinitions\pop-out-manager.xml</Package>
</Packages>
</Project>

View file

@ -0,0 +1,160 @@
#include <Joystick.h>
#include <NewEncoder.h>
// Rotary Encoder Inputs
#define CLK1 19
#define DT1 18
#define SW1 17
#define CLK2 2
#define DT2 3
#define SW2 4
#define VRx A0
#define VRy A1
#define JSW 8
// Rotatry encoder variables
int currentStateCLK1;
int lastStateCLK1;
int currentStateCLK2;
int lastStateCLK2;
String currentEvent = "";
String currentDir1 = "";
String currentDir2 = "";
boolean rotaryEncoderRotating1 = false;
boolean rotaryEncoderRotating2 = false;
unsigned long lastButton1Press = 0;
unsigned long lastButton2Press = 0;
Joystick joystick(VRx, VRy, JSW);
NewEncoder encoderLower(DT1, CLK1, -32768, 32767, 0, FULL_PULSE);
NewEncoder encoderUpper(DT2, CLK2, -32768, 32767, 0, FULL_PULSE);
int16_t prevEncoderValueLower;
int16_t prevEncoderValueUpper;
void setup() {
// Set encoder pins as inputs
pinMode(SW1, INPUT_PULLUP);
pinMode(SW2, INPUT_PULLUP);
// Setup joystick
joystick.initialize();
joystick.calibrate();
joystick.setSensivity(3);
// Setup Serial Monitor
Serial.begin(9600);
NewEncoder::EncoderState encoderState1;
NewEncoder::EncoderState encoderState2;
encoderLower.begin();
encoderLower.getState(encoderState1);
prevEncoderValueLower = encoderState1.currentValue;
encoderUpper.begin();
encoderUpper.getState(encoderState2);
prevEncoderValueUpper = encoderState2.currentValue;
}
void loop() {
int16_t currentEncoderValueLower;
int16_t currentEncoderValueUpper;
NewEncoder::EncoderState currentEncoderStateLower;
NewEncoder::EncoderState currentEncoderStateUpper;
// Read rotary encoder lower
if (encoderLower.getState(currentEncoderStateLower)) {
currentEncoderValueLower = currentEncoderStateLower.currentValue;
if (currentEncoderValueLower != prevEncoderValueLower) {
if(currentEncoderValueLower > prevEncoderValueLower){
Serial.println("EncoderLower:CW:" + String(currentEncoderValueLower - prevEncoderValueLower));
}
else{
Serial.println("EncoderLower:CCW:" + String(prevEncoderValueLower - currentEncoderValueLower));
}
prevEncoderValueLower = currentEncoderValueLower;
}
}
// Read rotary encoder upper
if (encoderUpper.getState(currentEncoderStateUpper)) {
currentEncoderValueUpper = currentEncoderStateUpper.currentValue;
if (currentEncoderValueUpper != prevEncoderValueUpper) {
if(currentEncoderValueUpper > prevEncoderValueUpper){
Serial.println("EncoderUpper:CW:" + String(currentEncoderValueUpper - prevEncoderValueUpper));
}
else{
Serial.println("EncoderUpper:CCW:" + String(prevEncoderValueUpper - currentEncoderValueUpper));
}
prevEncoderValueUpper = currentEncoderValueUpper;
}
}
// Read the rotary encoder button state
int btnState1 = digitalRead(SW1);
int btnState2 = digitalRead(SW2);
//If we detect LOW signal, button is pressed
if (btnState1 == LOW) {
//if 500ms have passed since last LOW pulse, it means that the
//button has been pressed, released and pressed again
if (millis() - lastButton1Press > 500) {
Serial.println("EncoderLower:SW");
}
// Remember last button press event
lastButton1Press = millis();
}
if (btnState2 == LOW) {
//if 500ms have passed since last LOW pulse, it means that the
//button has been pressed, released and pressed again
if (millis() - lastButton2Press > 500) {
Serial.println("EncoderUpper:SW");
}
// Remember last button press event
lastButton2Press = millis();
}
// Read joystick
if(joystick.isPressed())
{
Serial.println("Joystick:SW");
}
if(joystick.isReleased())
{
// left
if(joystick.isLeft())
{
Serial.println("Joystick:LEFT");
}
// right
if(joystick.isRight())
{
Serial.println("Joystick:RIGHT");
}
// up
if(joystick.isUp())
{
Serial.println("Joystick:UP");
}
// down
if(joystick.isDown())
{
Serial.println("Joystick:DOWN");
}
}
// slow down a bit
delay(200);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,197 @@
#include <Joystick.h>
#include <NewEncoder.h>
#include <Keypad.h>
// Rotary Encoder Inputs
#define CLK1 19
#define DT1 18
#define SW1 17
#define CLK2 2
#define DT2 3
#define SW2 4
#define VRx A0
#define VRy A1
const byte ROWS = 4;
const byte COLS = 4;
char hexaKeys[ROWS][COLS] = {
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}
};
byte rowPins[ROWS] = {31, 33, 35, 37};
byte colPins[COLS] = {39, 41, 43, 45};
Keypad customKeypad = Keypad(makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS);
// Rotatry encoder variables
int currentStateCLK1;
int lastStateCLK1;
int currentStateCLK2;
int lastStateCLK2;
String currentEvent = "";
String currentDir1 = "";
String currentDir2 = "";
boolean rotaryEncoderRotating1 = false;
boolean rotaryEncoderRotating2 = false;
unsigned long lastButton1Press = 0;
unsigned long lastButton2Press = 0;
Joystick joystick(VRx, VRy, 8);
NewEncoder encoderLower(DT1, CLK1, -32768, 32767, 0, FULL_PULSE);
NewEncoder encoderUpper(DT2, CLK2, -32768, 32767, 0, FULL_PULSE);
int16_t prevEncoderValueLower;
int16_t prevEncoderValueUpper;
void setup() {
// Set encoder pins as inputs
pinMode(SW1, INPUT_PULLUP);
pinMode(SW2, INPUT_PULLUP);
// Setup joystick
joystick.initialize();
joystick.calibrate();
joystick.setSensivity(3);
// Setup Serial Monitor
Serial.begin(9600);
NewEncoder::EncoderState encoderState1;
NewEncoder::EncoderState encoderState2;
encoderLower.begin();
encoderLower.getState(encoderState1);
prevEncoderValueLower = encoderState1.currentValue;
encoderUpper.begin();
encoderUpper.getState(encoderState2);
prevEncoderValueUpper = encoderState2.currentValue;
}
void loop() {
int16_t currentEncoderValueLower;
int16_t currentEncoderValueUpper;
NewEncoder::EncoderState currentEncoderStateLower;
NewEncoder::EncoderState currentEncoderStateUpper;
// Read rotary encoder lower
if (encoderLower.getState(currentEncoderStateLower)) {
currentEncoderValueLower = currentEncoderStateLower.currentValue;
if (currentEncoderValueLower != prevEncoderValueLower) {
if(currentEncoderValueLower > prevEncoderValueLower){
Serial.println("EncoderLower:CW:" + String(currentEncoderValueLower - prevEncoderValueLower));
}
else{
Serial.println("EncoderLower:CCW:" + String(prevEncoderValueLower - currentEncoderValueLower));
}
prevEncoderValueLower = currentEncoderValueLower;
}
}
// Read rotary encoder upper
if (encoderUpper.getState(currentEncoderStateUpper)) {
currentEncoderValueUpper = currentEncoderStateUpper.currentValue;
if (currentEncoderValueUpper != prevEncoderValueUpper) {
if(currentEncoderValueUpper > prevEncoderValueUpper){
Serial.println("EncoderUpper:CW:" + String(currentEncoderValueUpper - prevEncoderValueUpper));
}
else{
Serial.println("EncoderUpper:CCW:" + String(prevEncoderValueUpper - currentEncoderValueUpper));
}
prevEncoderValueUpper = currentEncoderValueUpper;
}
}
// Read the rotary encoder button state
int btnState1 = digitalRead(SW1);
int btnState2 = digitalRead(SW2);
//If we detect LOW signal, button is pressed
if (btnState1 == LOW) {
//if 500ms have passed since last LOW pulse, it means that the
//button has been pressed, released and pressed again
if (millis() - lastButton1Press > 500) {
Serial.println("EncoderLower:SW");
}
// Remember last button press event
lastButton1Press = millis();
}
if (btnState2 == LOW) {
//if 500ms have passed since last LOW pulse, it means that the
//button has been pressed, released and pressed again
if (millis() - lastButton2Press > 500) {
Serial.println("EncoderUpper:SW");
}
// Remember last button press event
lastButton2Press = millis();
}
// Read joystick
if(joystick.isPressed())
{
Serial.println("Joystick:SW");
}
// Read joystick (updated for joystick mounted at 90 degree)
if(joystick.isReleased())
{
// left
if(joystick.isLeft())
{
//Serial.println("Joystick:LEFT");
Serial.println("Joystick:UP");
}
// right
if(joystick.isRight())
{
//Serial.println("Joystick:RIGHT");
Serial.println("Joystick:DOWN");
}
// up
if(joystick.isUp())
{
//Serial.println("Joystick:UP");
Serial.println("Joystick:RIGHT");
}
// down
if(joystick.isDown())
{
//Serial.println("Joystick:DOWN");
Serial.println("Joystick:LEFT");
}
}
// Read keypad
char customKey = customKeypad.getKey();
if (customKey){
if(customKey == '#')
{
Serial.println("Keypad:KeyPound");
}
else if(customKey == '*')
{
Serial.println("Keypad:KeyAsterisk");
}
else
{
Serial.println("Keypad:Key" + String(customKey));
}
}
// slow down a bit
delay(100);
}

View file

@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AssemblyName>ArduinoAgent</AssemblyName>
<PackageId>MSFS 2020 Popout Panel Manager ArduinoAgent</PackageId>
<Product>MSFS 2020 Popout Panel Manager ArduinoAgent</Product>
<Authors>Stanley Kwok</Authors>
<Company>Stanley Kwok</Company>
<Copyright>Stanley Kwok 2021</Copyright>
<PackageProjectUrl>https://github.com/hawkeye-stan/msfs-popout-panel-manager</PackageProjectUrl>
<RootNamespace>MSFSPopoutPanelManager.ArduinoAgent</RootNamespace>
<Platforms>x64</Platforms>
<Version>3.4.0.0</Version>
<AssemblyVersion>3.4.0.0</AssemblyVersion>
<FileVersion>3.4.0.0</FileVersion>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<DebugType>Embedded</DebugType>
<Configurations>Debug;Release;DebugTouchPanel;ReleaseTouchPanel</Configurations>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='DebugTouchPanel|x64'">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Ports" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,62 @@
using System;
namespace MSFSPopoutPanelManager.ArduinoAgent
{
public class ArduinoInputData
{
public ArduinoInputData(string inputName, string inputAction, int acceleration)
{
InputName = (InputName)Enum.Parse(typeof(InputName), inputName);
InputAction = (InputAction)Enum.Parse(typeof(InputAction), inputAction);
Acceleration = acceleration;
}
public InputName InputName { get; set; }
public InputAction InputAction { get; set; }
public int Acceleration { get; set; }
}
public enum InputAction
{
NONE,
// Rotary Encoder
CW,
CCW,
SW,
// Joystick
UP,
DOWN,
LEFT,
RIGHT,
// Keypad
Key1,
Key2,
Key3,
Key4,
Key5,
Key6,
Key7,
Key8,
Key9,
Key0,
KeyA,
KeyB,
KeyC,
KeyD,
KeyAsterisk,
KeyPound
}
public enum InputName
{
EncoderLower,
EncoderUpper,
Joystick,
Keypad
}
}

View file

@ -0,0 +1,143 @@
using MSFSPopoutPanelManager.Shared;
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Threading;
namespace MSFSPopoutPanelManager.ArduinoAgent
{
public class ArduinoProvider
{
private const string ARDUINO_COM_PORT = "COM3";
private const int ARDUINO_BAUD_RATE = 9600;
private SerialPort _serialPort;
public event EventHandler<bool> OnConnectionChanged;
public event EventHandler<ArduinoInputData> OnDataReceived;
public ArduinoProvider()
{
try
{
_serialPort = new SerialPort
{
PortName = ARDUINO_COM_PORT,
BaudRate = ARDUINO_BAUD_RATE
};
_serialPort.DataReceived += DataReceived;
}
catch (Exception ex)
{
FileLogger.WriteException($"Arduino Error: {ex.Message}", null);
}
}
public void Start()
{
if (!IsConnected)
{
try
{
_serialPort.Open();
OnConnectionChanged?.Invoke(this, true);
FileLogger.WriteLog($"Arduino connected.", StatusMessageType.Info);
}
catch (Exception ex)
{
FileLogger.WriteException($"Arduino Connection Error - {ex.Message}", ex);
}
}
}
public void Stop()
{
if (_serialPort.IsOpen)
{
try
{
_serialPort.Close();
FileLogger.WriteLog($"Arduino disconnected.", StatusMessageType.Info);
}
catch (Exception ex)
{
FileLogger.WriteException($"Arduino Connection Error - {ex.Message}", ex);
}
}
OnConnectionChanged?.Invoke(this, false);
}
public void SendToArduino(string data)
{
try
{
if (IsConnected)
_serialPort.WriteLine(data);
}
catch (Exception ex)
{
FileLogger.WriteException($"Arduino Connection Error - {ex.Message}", ex);
}
}
private void DataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (_serialPort.IsOpen)
{
try
{
var dataEvents = new List<ArduinoInputData>();
int byteToRead = _serialPort.BytesToRead;
while (byteToRead > 0)
{
var message = _serialPort.ReadTo("\r\n");
var data = message.Split(":");
ArduinoInputData dataEvent;
// Calculate acceleration
if (data.Length == 3)
{
var accelerationValue = Convert.ToInt32(data[2]);
//dataEvent = new ArduinoInputData(data[0], data[1], accelerationValue == 1 ? 1 : accelerationValue / 2);
dataEvent = new ArduinoInputData(data[0], data[1], accelerationValue <= 2 ? 1 : accelerationValue);
}
else
{
dataEvent = new ArduinoInputData(data[0], data[1], 1);
}
dataEvents.Add(dataEvent);
byteToRead = _serialPort.BytesToRead;
}
foreach (var evt in dataEvents)
{
OnDataReceived?.Invoke(this, evt);
Thread.Sleep(10);
}
}
catch
{
_serialPort.DiscardInBuffer();
}
}
}
private bool IsConnected
{
get
{
if (_serialPort == null)
return false;
return _serialPort.IsOpen;
}
}
}
}

View file

@ -1,22 +0,0 @@
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)),
("TrackIREnable", "TRACK IR ENABLE", "Bool", SIMCONNECT_DATATYPE.FLOAT64, typeof(bool)),
("AtcOnParkingSpot", "ATC ON PARKING SPOT", "Bool", SIMCONNECT_DATATYPE.FLOAT64, typeof(bool))
};
return def;
}
}
}

View file

@ -1,28 +0,0 @@
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,
NONE
};
}

View file

@ -1,36 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0-windows</TargetFramework>
<AssemblyName>FsConnector</AssemblyName>
<PackageId>MSFS 2020 Popout Panel Manager FsConnector</PackageId>
<Product>MSFS 2020 Popout Panel Manager FsConnector</Product>
<Version>3.3.7.0</Version>
<Authors>Stanley Kwok</Authors>
<Company>Stanley Kwok</Company>
<Copyright>Stanley Kwok 2021</Copyright>
<PackageProjectUrl>https://github.com/hawkeye-stan/msfs-popout-panel-manager</PackageProjectUrl>
<RootNamespace>MSFSPopoutPanelManager.FsConnector</RootNamespace>
<Platforms>x64</Platforms>
<AssemblyVersion>3.3.7.0</AssemblyVersion>
<FileVersion>3.3.7.0</FileVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.FlightSimulator.SimConnect">
<HintPath>Resources\Managed\Microsoft.FlightSimulator.SimConnect.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<ContentWithTargetPath Include="resources\SimConnect.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>SimConnect.dll</TargetPath>
</ContentWithTargetPath>
</ItemGroup>
</Project>

Binary file not shown.

View file

@ -1,15 +0,0 @@
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;
}
}

View file

@ -1,209 +0,0 @@
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<EventArgs<dynamic>> OnReceivedData;
public event EventHandler OnConnected;
public event EventHandler OnDisconnected;
public event EventHandler<EventArgs<SimConnectSystemEvent>> 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<SimConnectStruct>(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}");
}
}
}
public void SetDataObject(SimConnectStruct simConnectStruct)
{
if (_simConnect != null)
{
try
{
_simConnect.SetDataOnSimObject(SimConnectDefinition.SimConnectDataStruct, SimConnect.SIMCONNECT_OBJECT_ID_USER, SIMCONNECT_DATA_SET_FLAG.DEFAULT, simConnectStruct);
}
catch (Exception ex)
{
Debug.WriteLine($"Unable to set SimConnect variable: {ex.Message}");
}
}
}
private void HandleOnRecvQuit(SimConnect sender, SIMCONNECT_RECV data)
{
OnDisconnected?.Invoke(this, null);
StopAndReconnect();
// Try to reconnect again
_timer.Enabled = true;
}
private void HandleOnRecvException(SimConnect sender, SIMCONNECT_RECV_EXCEPTION data)
{
var exception = (SIMCONNECT_EXCEPTION)data.dwException;
switch (exception)
{
case SIMCONNECT_EXCEPTION.DATA_ERROR:
case SIMCONNECT_EXCEPTION.NAME_UNRECOGNIZED:
case SIMCONNECT_EXCEPTION.EVENT_ID_DUPLICATE:
return;
default:
Debug.WriteLine($"MSFS Error - {exception}");
break;
}
}
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<dynamic>(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);
OnReceiveSystemEvent?.Invoke(this, new EventArgs<SimConnectSystemEvent>(systemEvent));
}
}
}

View file

@ -5,14 +5,8 @@ VisualStudioVersion = 17.2.32602.215
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfApp", "WpfApp\WpfApp.csproj", "{54712A0A-B344-45E4-85C4-0A913305A0E6}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfApp", "WpfApp\WpfApp.csproj", "{54712A0A-B344-45E4-85C4-0A913305A0E6}"
EndProject 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}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "Shared\Shared.csproj", "{4BDDE1F9-FBDD-479A-B88E-B27D0513C046}"
EndProject 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}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{15FC98CD-0A69-437B-A5E5-67D025DB5CDC}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore .gitignore = .gitignore
@ -23,42 +17,176 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
VERSION.md = VERSION.md VERSION.md = VERSION.md
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsAgent", "WindowsAgent\WindowsAgent.csproj", "{BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArduinoAgent", "ArduinoAgent\ArduinoAgent.csproj", "{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimconnectAgent", "SimconnectAgent\SimconnectAgent.csproj", "{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebServer", "WebServer\WebServer.csproj", "{D20EA590-22C2-4F93-AC25-AF6960F97E03}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserDataAgent", "UserDataAgent\UserDataAgent.csproj", "{677B6DF0-8E19-4B08-8480-F7C365B6BB89}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orchestration", "Orchestration\Orchestration.csproj", "{9CD4014F-B3EA-4E67-9329-9679931E2688}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TouchPanelAgent", "TouchPanelAgent\TouchPanelAgent.csproj", "{2F36A227-C92C-4B42-B1E4-480015492357}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64 Debug|x64 = Debug|x64
DebugTouchPanel|Any CPU = DebugTouchPanel|Any CPU
DebugTouchPanel|x64 = DebugTouchPanel|x64
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64 Release|x64 = Release|x64
ReleaseTouchPanel|Any CPU = ReleaseTouchPanel|Any CPU
ReleaseTouchPanel|x64 = ReleaseTouchPanel|x64
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{54712A0A-B344-45E4-85C4-0A913305A0E6}.Debug|Any CPU.ActiveCfg = Debug|x64
{54712A0A-B344-45E4-85C4-0A913305A0E6}.Debug|Any CPU.Build.0 = Debug|x64
{54712A0A-B344-45E4-85C4-0A913305A0E6}.Debug|x64.ActiveCfg = Debug|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}.Debug|x64.Build.0 = Debug|x64
{54712A0A-B344-45E4-85C4-0A913305A0E6}.DebugTouchPanel|Any CPU.ActiveCfg = DebugTouchPanel|x64
{54712A0A-B344-45E4-85C4-0A913305A0E6}.DebugTouchPanel|Any CPU.Build.0 = DebugTouchPanel|x64
{54712A0A-B344-45E4-85C4-0A913305A0E6}.DebugTouchPanel|x64.ActiveCfg = DebugTouchPanel|x64
{54712A0A-B344-45E4-85C4-0A913305A0E6}.DebugTouchPanel|x64.Build.0 = DebugTouchPanel|x64
{54712A0A-B344-45E4-85C4-0A913305A0E6}.Release|Any CPU.ActiveCfg = Release|x64
{54712A0A-B344-45E4-85C4-0A913305A0E6}.Release|Any CPU.Build.0 = Release|x64
{54712A0A-B344-45E4-85C4-0A913305A0E6}.Release|x64.ActiveCfg = Release|x64 {54712A0A-B344-45E4-85C4-0A913305A0E6}.Release|x64.ActiveCfg = Release|x64
{54712A0A-B344-45E4-85C4-0A913305A0E6}.Release|x64.Build.0 = Release|x64 {54712A0A-B344-45E4-85C4-0A913305A0E6}.Release|x64.Build.0 = Release|x64
{4A778C1A-3782-4312-842D-33AA58A9D6D4}.Debug|x64.ActiveCfg = Debug|x64 {54712A0A-B344-45E4-85C4-0A913305A0E6}.ReleaseTouchPanel|Any CPU.ActiveCfg = ReleaseTouchPanel|x64
{4A778C1A-3782-4312-842D-33AA58A9D6D4}.Debug|x64.Build.0 = Debug|x64 {54712A0A-B344-45E4-85C4-0A913305A0E6}.ReleaseTouchPanel|Any CPU.Build.0 = ReleaseTouchPanel|x64
{4A778C1A-3782-4312-842D-33AA58A9D6D4}.Release|x64.ActiveCfg = Release|x64 {54712A0A-B344-45E4-85C4-0A913305A0E6}.ReleaseTouchPanel|x64.ActiveCfg = ReleaseTouchPanel|x64
{4A778C1A-3782-4312-842D-33AA58A9D6D4}.Release|x64.Build.0 = Release|x64 {54712A0A-B344-45E4-85C4-0A913305A0E6}.ReleaseTouchPanel|x64.Build.0 = ReleaseTouchPanel|x64
{4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.Debug|Any CPU.ActiveCfg = Debug|x64
{4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.Debug|Any CPU.Build.0 = Debug|x64
{4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.Debug|x64.ActiveCfg = Debug|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}.Debug|x64.Build.0 = Debug|x64
{4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.DebugTouchPanel|Any CPU.ActiveCfg = DebugTouchPanel|x64
{4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.DebugTouchPanel|Any CPU.Build.0 = DebugTouchPanel|x64
{4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.DebugTouchPanel|x64.ActiveCfg = DebugTouchPanel|x64
{4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.DebugTouchPanel|x64.Build.0 = DebugTouchPanel|x64
{4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.Release|Any CPU.ActiveCfg = Release|x64
{4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.Release|Any CPU.Build.0 = Release|x64
{4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.Release|x64.ActiveCfg = Release|x64 {4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.Release|x64.ActiveCfg = Release|x64
{4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.Release|x64.Build.0 = Release|x64 {4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.Release|x64.Build.0 = Release|x64
{933E7D03-883D-4970-9AD7-7A2B5D6C3671}.Debug|x64.ActiveCfg = Debug|x64 {4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.ReleaseTouchPanel|Any CPU.ActiveCfg = ReleaseTouchPanel|x64
{933E7D03-883D-4970-9AD7-7A2B5D6C3671}.Debug|x64.Build.0 = Debug|x64 {4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.ReleaseTouchPanel|Any CPU.Build.0 = ReleaseTouchPanel|x64
{933E7D03-883D-4970-9AD7-7A2B5D6C3671}.Release|x64.ActiveCfg = Release|x64 {4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.ReleaseTouchPanel|x64.ActiveCfg = ReleaseTouchPanel|x64
{933E7D03-883D-4970-9AD7-7A2B5D6C3671}.Release|x64.Build.0 = Release|x64 {4BDDE1F9-FBDD-479A-B88E-B27D0513C046}.ReleaseTouchPanel|x64.Build.0 = ReleaseTouchPanel|x64
{023426F4-9FD2-4198-B9F4-83F0B55B88FC}.Debug|x64.ActiveCfg = Debug|x64 {BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.Debug|Any CPU.ActiveCfg = Debug|x64
{023426F4-9FD2-4198-B9F4-83F0B55B88FC}.Debug|x64.Build.0 = Debug|x64 {BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.Debug|Any CPU.Build.0 = Debug|x64
{023426F4-9FD2-4198-B9F4-83F0B55B88FC}.Release|x64.ActiveCfg = Release|x64 {BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.Debug|x64.ActiveCfg = Debug|x64
{023426F4-9FD2-4198-B9F4-83F0B55B88FC}.Release|x64.Build.0 = Release|x64 {BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.Debug|x64.Build.0 = Debug|x64
{BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.DebugTouchPanel|Any CPU.ActiveCfg = DebugTouchPanel|x64
{BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.DebugTouchPanel|Any CPU.Build.0 = DebugTouchPanel|x64
{BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.DebugTouchPanel|x64.ActiveCfg = DebugTouchPanel|x64
{BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.DebugTouchPanel|x64.Build.0 = DebugTouchPanel|x64
{BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.Release|Any CPU.ActiveCfg = Release|x64
{BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.Release|Any CPU.Build.0 = Release|x64
{BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.Release|x64.ActiveCfg = Release|x64
{BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.Release|x64.Build.0 = Release|x64
{BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.ReleaseTouchPanel|Any CPU.ActiveCfg = ReleaseTouchPanel|x64
{BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.ReleaseTouchPanel|Any CPU.Build.0 = ReleaseTouchPanel|x64
{BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.ReleaseTouchPanel|x64.ActiveCfg = ReleaseTouchPanel|x64
{BDD878A3-EF5B-43C7-94B7-CEFA04C67A9E}.ReleaseTouchPanel|x64.Build.0 = ReleaseTouchPanel|x64
{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}.Debug|Any CPU.ActiveCfg = Debug|x64
{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}.Debug|Any CPU.Build.0 = Debug|x64
{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}.Debug|x64.ActiveCfg = Debug|x64
{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}.DebugTouchPanel|Any CPU.ActiveCfg = DebugTouchPanel|x64
{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}.DebugTouchPanel|Any CPU.Build.0 = DebugTouchPanel|x64
{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}.DebugTouchPanel|x64.ActiveCfg = DebugTouchPanel|x64
{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}.DebugTouchPanel|x64.Build.0 = DebugTouchPanel|x64
{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}.Release|Any CPU.ActiveCfg = Release|x64
{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}.Release|Any CPU.Build.0 = Release|x64
{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}.Release|x64.ActiveCfg = Release|x64
{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}.ReleaseTouchPanel|Any CPU.ActiveCfg = ReleaseTouchPanel|x64
{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}.ReleaseTouchPanel|Any CPU.Build.0 = ReleaseTouchPanel|x64
{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}.ReleaseTouchPanel|x64.ActiveCfg = ReleaseTouchPanel|x64
{E72F813F-EE30-4384-B02F-EB5D4BCFEC49}.ReleaseTouchPanel|x64.Build.0 = ReleaseTouchPanel|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.Debug|Any CPU.ActiveCfg = Debug|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.Debug|Any CPU.Build.0 = Debug|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.Debug|x64.ActiveCfg = Debug|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.Debug|x64.Build.0 = Debug|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.DebugTouchPanel|Any CPU.ActiveCfg = DebugTouchPanel|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.DebugTouchPanel|Any CPU.Build.0 = DebugTouchPanel|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.DebugTouchPanel|x64.ActiveCfg = DebugTouchPanel|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.DebugTouchPanel|x64.Build.0 = DebugTouchPanel|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.Release|Any CPU.ActiveCfg = Release|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.Release|Any CPU.Build.0 = Release|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.Release|x64.ActiveCfg = Release|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.Release|x64.Build.0 = Release|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.ReleaseTouchPanel|Any CPU.ActiveCfg = ReleaseTouchPanel|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.ReleaseTouchPanel|Any CPU.Build.0 = ReleaseTouchPanel|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.ReleaseTouchPanel|x64.ActiveCfg = ReleaseTouchPanel|x64
{1BBF7803-BA8D-4AEF-8319-061E93AE5F3D}.ReleaseTouchPanel|x64.Build.0 = ReleaseTouchPanel|x64
{D20EA590-22C2-4F93-AC25-AF6960F97E03}.Debug|Any CPU.ActiveCfg = Debug|x64
{D20EA590-22C2-4F93-AC25-AF6960F97E03}.Debug|Any CPU.Build.0 = Debug|x64
{D20EA590-22C2-4F93-AC25-AF6960F97E03}.Debug|x64.ActiveCfg = Debug|x64
{D20EA590-22C2-4F93-AC25-AF6960F97E03}.DebugTouchPanel|Any CPU.ActiveCfg = DebugTouchPanel|x64
{D20EA590-22C2-4F93-AC25-AF6960F97E03}.DebugTouchPanel|Any CPU.Build.0 = DebugTouchPanel|x64
{D20EA590-22C2-4F93-AC25-AF6960F97E03}.DebugTouchPanel|x64.ActiveCfg = DebugTouchPanel|x64
{D20EA590-22C2-4F93-AC25-AF6960F97E03}.DebugTouchPanel|x64.Build.0 = DebugTouchPanel|x64
{D20EA590-22C2-4F93-AC25-AF6960F97E03}.Release|Any CPU.ActiveCfg = Release|x64
{D20EA590-22C2-4F93-AC25-AF6960F97E03}.Release|Any CPU.Build.0 = Release|x64
{D20EA590-22C2-4F93-AC25-AF6960F97E03}.Release|x64.ActiveCfg = Release|x64
{D20EA590-22C2-4F93-AC25-AF6960F97E03}.ReleaseTouchPanel|Any CPU.ActiveCfg = ReleaseTouchPanel|x64
{D20EA590-22C2-4F93-AC25-AF6960F97E03}.ReleaseTouchPanel|Any CPU.Build.0 = ReleaseTouchPanel|x64
{D20EA590-22C2-4F93-AC25-AF6960F97E03}.ReleaseTouchPanel|x64.ActiveCfg = ReleaseTouchPanel|x64
{D20EA590-22C2-4F93-AC25-AF6960F97E03}.ReleaseTouchPanel|x64.Build.0 = ReleaseTouchPanel|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.Debug|Any CPU.ActiveCfg = Debug|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.Debug|Any CPU.Build.0 = Debug|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.Debug|x64.ActiveCfg = Debug|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.Debug|x64.Build.0 = Debug|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.DebugTouchPanel|Any CPU.ActiveCfg = DebugTouchPanel|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.DebugTouchPanel|Any CPU.Build.0 = DebugTouchPanel|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.DebugTouchPanel|x64.ActiveCfg = DebugTouchPanel|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.DebugTouchPanel|x64.Build.0 = DebugTouchPanel|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.Release|Any CPU.ActiveCfg = Release|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.Release|Any CPU.Build.0 = Release|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.Release|x64.ActiveCfg = Release|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.Release|x64.Build.0 = Release|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.ReleaseTouchPanel|Any CPU.ActiveCfg = ReleaseTouchPanel|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.ReleaseTouchPanel|Any CPU.Build.0 = ReleaseTouchPanel|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.ReleaseTouchPanel|x64.ActiveCfg = ReleaseTouchPanel|x64
{677B6DF0-8E19-4B08-8480-F7C365B6BB89}.ReleaseTouchPanel|x64.Build.0 = ReleaseTouchPanel|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.Debug|Any CPU.ActiveCfg = Debug|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.Debug|Any CPU.Build.0 = Debug|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.Debug|x64.ActiveCfg = Debug|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.Debug|x64.Build.0 = Debug|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.DebugTouchPanel|Any CPU.ActiveCfg = DebugTouchPanel|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.DebugTouchPanel|Any CPU.Build.0 = DebugTouchPanel|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.DebugTouchPanel|x64.ActiveCfg = DebugTouchPanel|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.DebugTouchPanel|x64.Build.0 = DebugTouchPanel|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.Release|Any CPU.ActiveCfg = Release|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.Release|Any CPU.Build.0 = Release|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.Release|x64.ActiveCfg = Release|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.Release|x64.Build.0 = Release|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.ReleaseTouchPanel|Any CPU.ActiveCfg = ReleaseTouchPanel|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.ReleaseTouchPanel|Any CPU.Build.0 = ReleaseTouchPanel|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.ReleaseTouchPanel|x64.ActiveCfg = ReleaseTouchPanel|x64
{9CD4014F-B3EA-4E67-9329-9679931E2688}.ReleaseTouchPanel|x64.Build.0 = ReleaseTouchPanel|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.Debug|Any CPU.ActiveCfg = Debug|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.Debug|Any CPU.Build.0 = Debug|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.Debug|x64.ActiveCfg = Debug|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.Debug|x64.Build.0 = Debug|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.DebugTouchPanel|Any CPU.ActiveCfg = DebugTouchPanel|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.DebugTouchPanel|Any CPU.Build.0 = DebugTouchPanel|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.DebugTouchPanel|x64.ActiveCfg = DebugTouchPanel|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.DebugTouchPanel|x64.Build.0 = DebugTouchPanel|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.Release|Any CPU.ActiveCfg = Release|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.Release|Any CPU.Build.0 = Release|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.Release|x64.ActiveCfg = Release|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.Release|x64.Build.0 = Release|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.ReleaseTouchPanel|Any CPU.ActiveCfg = ReleaseTouchPanel|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.ReleaseTouchPanel|Any CPU.Build.0 = ReleaseTouchPanel|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.ReleaseTouchPanel|x64.ActiveCfg = ReleaseTouchPanel|x64
{2F36A227-C92C-4B42-B1E4-480015492357}.ReleaseTouchPanel|x64.Build.0 = ReleaseTouchPanel|x64
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{D8938324-934B-4602-B30E-66B60365B426} = {15FC98CD-0A69-437B-A5E5-67D025DB5CDC}
{E2AF3E4D-F5B6-44C0-8E15-528206E9F3C7} = {D8938324-934B-4602-B30E-66B60365B426}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D1607553-61E8-4ED6-B382-ABA6F6944586} SolutionGuid = {D1607553-61E8-4ED6-B382-ABA6F6944586}
EndGlobalSection EndGlobalSection

View file

@ -1,169 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using MSFSPopoutPanelManager.Shared;
using Newtonsoft.Json;
using System;
using System.IO;
namespace MSFSPopoutPanelManager.Model
{
public class AppSetting : ObservableObject
{
private const string APP_SETTING_DATA_FILENAME = "appsettingdata.json";
private bool _saveEnabled;
public event EventHandler<EventArgs<bool>> AlwaysOnTopChanged;
public event EventHandler<EventArgs<bool>> AutoPopOutPanelsChanged;
public AppSetting()
{
// Set defaults
AutoUpdaterUrl = "https://raw.githubusercontent.com/hawkeye-stan/msfs-popout-panel-manager/master/autoupdate.xml";
//AutoUpdaterUrl = "https://raw.githubusercontent.com/hawkeye-stan/AutoUpdateTest/main/autoupdate.xml"; // Test URL against test repo
LastUsedProfileId = -1;
MinimizeToTray = false;
AlwaysOnTop = true;
UseAutoPanning = true;
AutoPanningKeyBinding = "0";
StartMinimized = false;
IncludeBuiltInPanel = false;
AutoDisableTrackIR = true;
AutoPopOutPanels = true;
AutoPopOutPanelsWaitDelay = new AutoPopOutPanelsWaitDelay();
}
public void Load()
{
var appSetting = ReadAppSetting();
this.AutoUpdaterUrl = appSetting.AutoUpdaterUrl;
this.LastUsedProfileId = appSetting.LastUsedProfileId;
this.MinimizeToTray = appSetting.MinimizeToTray;
this.AlwaysOnTop = appSetting.AlwaysOnTop;
this.UseAutoPanning = appSetting.UseAutoPanning;
this.AutoPanningKeyBinding = appSetting.AutoPanningKeyBinding;
this.StartMinimized = appSetting.StartMinimized;
this.IncludeBuiltInPanel = appSetting.IncludeBuiltInPanel;
this.AutoDisableTrackIR = appSetting.AutoDisableTrackIR;
this.AutoPopOutPanels = appSetting.AutoPopOutPanels;
this.AutoPopOutPanelsWaitDelay = appSetting.AutoPopOutPanelsWaitDelay;
AutoPopOutPanelsWaitDelay.DataChanged += (e, source) => WriteAppSetting(this);
_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>((bool)after));
break;
case "AutoPopOutPanels":
AutoPopOutPanelsChanged?.Invoke(this, new EventArgs<bool>((bool)after));
break;
}
}
public string AutoUpdaterUrl { get; set; }
public int LastUsedProfileId { get; set; }
public bool MinimizeToTray { get; set; }
public bool AlwaysOnTop { get; set; }
public bool UseAutoPanning { get; set; }
public string AutoPanningKeyBinding { get; set; }
public bool StartMinimized { get; set; }
public bool IncludeBuiltInPanel { get; set; }
public bool AutoPopOutPanels { get; set; }
public bool AutoDisableTrackIR { 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<AppSetting>(reader.ReadToEnd());
}
}
catch
{
// 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 : ObservableObject
{
public event EventHandler<EventArgs<string>> DataChanged;
public AutoPopOutPanelsWaitDelay()
{
ReadyToFlyButton = 4;
InitialCockpitView = 1;
InstrumentationPowerOn = 1;
}
public int ReadyToFlyButton { get; set; }
public int InitialCockpitView { get; set; }
public int InstrumentationPowerOn { get; set; }
public void OnPropertyChanged(string propertyName, object before, object after)
{
DataChanged?.Invoke(this, new EventArgs<string>(propertyName));
}
}
}

View file

@ -1,29 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0-windows</TargetFramework>
<RootNamespace>MSFSPopoutPanelManager.Model</RootNamespace>
<AssemblyName>Model</AssemblyName>
<PackageId>MSFS 2020 Popout Panel Manager Model</PackageId>
<Version>3.3.7.0</Version>
<Authors>Stanley Kwok</Authors>
<Company>Stanley Kwok</Company>
<Copyright>Stanley Kwok 2021</Copyright>
<Product>MSFS 2020 Popout Panel Manager Model</Product>
<PackageProjectUrl>https://github.com/hawkeye-stan/msfs-popout-panel-manager</PackageProjectUrl>
<Platforms>x64</Platforms>
<AssemblyVersion>3.3.7.0</AssemblyVersion>
<FileVersion>3.3.7.0</FileVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="7.1.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>
</Project>

View file

@ -1,70 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Newtonsoft.Json;
using System;
namespace MSFSPopoutPanelManager.Model
{
public class PanelConfig : ObservableObject
{
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; }
public bool FullScreen { get; set; }
public bool TouchEnabled { get; set; }
[JsonIgnore]
public bool IsCustomPopout { get { return PanelType == PanelType.CustomPopout; } }
[JsonIgnore]
public IntPtr PanelHandle { get; set; }
[JsonIgnore]
public bool IsLockable
{
get
{
switch (PanelType)
{
case PanelType.CustomPopout:
case PanelType.BuiltInPopout:
case PanelType.MSFSTouchPanel:
return true;
default:
return false;
}
}
}
[JsonIgnore]
public bool HasTouchableEvent
{
get
{
switch (PanelType)
{
case PanelType.CustomPopout:
case PanelType.BuiltInPopout:
return true;
default:
return false;
}
}
}
}
}

View file

@ -1,18 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Newtonsoft.Json;
using System;
namespace MSFSPopoutPanelManager.Model
{
public class PanelSourceCoordinate : ObservableObject
{
public int PanelIndex { get; set; }
public int X { get; set; }
public int Y { get; set; }
[JsonIgnore]
public IntPtr PanelHandle { get; set; }
}
}

View file

@ -1,100 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Newtonsoft.Json;
using System;
using System.Collections.ObjectModel;
namespace MSFSPopoutPanelManager.Model
{
public class UserProfile : ObservableObject
{
public UserProfile()
{
PanelSourceCoordinates = new ObservableCollection<PanelSourceCoordinate>();
PanelConfigs = new ObservableCollection<PanelConfig>();
BindingAircraftLiveries = new ObservableCollection<string>();
IsLocked = false;
}
public int ProfileId { get; set; }
public string ProfileName { get; set; }
[JsonConverter(typeof(SingleValueArrayConvertor<string>))]
public ObservableCollection<string> BindingAircraftLiveries { get; set; }
public bool IsLocked { get; set; }
public ObservableCollection<PanelSourceCoordinate> PanelSourceCoordinates { get; set; }
public ObservableCollection<PanelConfig> 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 HasBindingAircraftLiveries
{
get { return BindingAircraftLiveries.Count > 0; }
}
[JsonIgnore]
public bool HasPanelSourceCoordinates
{
get { return PanelSourceCoordinates.Count > 0; }
}
#region Legacy Properties
// Support pre-Version 3.3 tag for one time conversion
[JsonConverter(typeof(SingleValueArrayConvertor<string>))]
public ObservableCollection<string> BindingPlaneTitle
{
set { BindingAircraftLiveries = value; }
}
#endregion
}
public class SingleValueArrayConvertor<T> : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
object val = new object();
if (reader.TokenType == JsonToken.String)
{
var instance = (string)serializer.Deserialize(reader, typeof(string));
val = new ObservableCollection<string>() { instance };
}
else if (reader.TokenType == JsonToken.StartArray)
{
val = serializer.Deserialize(reader, objectType);
}
else if (reader.TokenType == JsonToken.Null)
{
val = new ObservableCollection<string>();
}
return val;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}
}

View file

@ -0,0 +1,53 @@
using MSFSPopoutPanelManager.Shared;
using MSFSPopoutPanelManager.UserDataAgent;
using System;
namespace MSFSPopoutPanelManager.Orchestration
{
public class AppSettingData : ObservableObject
{
public event EventHandler AppSettingUpdated;
public event EventHandler<bool> AlwaysOnTopChanged;
public event EventHandler<bool> AutoPopOutPanelsChanged;
public event EventHandler<bool> TouchPanelIntegrationChanged;
public AppSetting AppSetting { get; private set; }
public void ReadSettings()
{
AppSetting = AppSettingManager.ReadAppSetting();
if (AppSetting == null)
{
AppSetting = new AppSetting();
AppSettingManager.WriteAppSetting(AppSetting);
}
// Autosave data
AppSetting.PropertyChanged += (sender, e) =>
{
var arg = e as PropertyChangedExtendedEventArgs;
if (arg.OldValue != arg.NewValue)
{
AppSettingManager.WriteAppSetting(AppSetting);
switch (arg.PropertyName)
{
case "AlwaysOnTop":
AlwaysOnTopChanged?.Invoke(this, (bool)arg.NewValue);
break;
case "AutoPopOutPanels":
AutoPopOutPanelsChanged?.Invoke(this, (bool)arg.NewValue);
break;
case "EnableTouchPanelIntegration":
TouchPanelIntegrationChanged?.Invoke(this, (bool)arg.NewValue);
break;
}
AppSettingUpdated?.Invoke(this, null);
}
};
}
}
}

View file

@ -0,0 +1,42 @@
using MSFSPopoutPanelManager.Shared;
using System;
using System.ComponentModel;
namespace MSFSPopoutPanelManager.Orchestration
{
public class FlightSimData : ObservableObject
{
public event PropertyChangedEventHandler CurrentMsfsPlaneTitleChanged;
public string CurrentMsfsPlaneTitle { get; set; }
public bool HasCurrentMsfsPlaneTitle
{
get { return !String.IsNullOrEmpty(CurrentMsfsPlaneTitle); }
}
public bool ElectricalMasterBatteryStatus { get; set; }
public bool IsSimulatorStarted { get; set; }
public bool IsEnteredFlight { get; set; }
public new void OnPropertyChanged(string propertyName, object oldValue, object newValue)
{
if (oldValue != newValue)
{
if (propertyName == "CurrentMsfsPlaneTitle")
CurrentMsfsPlaneTitleChanged?.Invoke(this, null);
base.OnPropertyChanged(propertyName, oldValue, newValue);
}
}
public void ClearData()
{
CurrentMsfsPlaneTitle = null;
ElectricalMasterBatteryStatus = false;
IsEnteredFlight = false;
}
}
}

View file

@ -0,0 +1,124 @@
using MSFSPopoutPanelManager.Shared;
using MSFSPopoutPanelManager.SimConnectAgent;
using System;
namespace MSFSPopoutPanelManager.Orchestration
{
public class FlightSimOrchestrator : ObservableObject
{
private SimConnectProvider _simConnectProvider;
public FlightSimOrchestrator()
{
_simConnectProvider = new SimConnectProvider();
}
internal ProfileData ProfileData { get; set; }
internal AppSettingData AppSettingData { get; set; }
internal FlightSimData FlightSimData { get; set; }
internal SimConnectProvider SimConnectProvider { get { return _simConnectProvider; } }
public event EventHandler OnSimulatorStarted;
public event EventHandler OnSimulatorStopped;
public event EventHandler OnFlightStarted;
public event EventHandler OnFlightStopped;
public event EventHandler OnFlightStartedForAutoPopOut;
public void StartSimConnectServer()
{
_simConnectProvider.OnConnected += (sender, e) =>
{
FlightSimData.IsSimulatorStarted = true;
OnSimulatorStarted?.Invoke(this, null);
};
_simConnectProvider.OnDisconnected += (sender, e) =>
{
FlightSimData.IsSimulatorStarted = false;
FlightSimData.ClearData();
OnSimulatorStopped?.Invoke(this, null);
};
_simConnectProvider.OnSimConnectDataRefreshed += (sender, e) =>
{
var title = Convert.ToString(e.Find(d => d.PropName == "Title").Value);
var electricalMasterBattery = Convert.ToBoolean(e.Find(d => d.PropName == "ElectricalMasterBattery").Value);
// Automatic switching of active profile when SimConnect active aircraft livery changes
if (FlightSimData.CurrentMsfsPlaneTitle != title)
{
FlightSimData.CurrentMsfsPlaneTitle = title;
ProfileData.AutoSwitchProfile(title);
}
if (electricalMasterBattery != FlightSimData.ElectricalMasterBatteryStatus)
FlightSimData.ElectricalMasterBatteryStatus = electricalMasterBattery;
};
_simConnectProvider.OnFlightStarted += (sender, e) => OnFlightStarted?.Invoke(this, null);
_simConnectProvider.OnFlightStopped += HandleOnFlightStopped;
_simConnectProvider.Start();
}
public void EndSimConnectServer(bool appExit)
{
_simConnectProvider.Stop(appExit);
}
public void TurnOnTrackIR()
{
if (AppSettingData.AppSetting.AutoDisableTrackIR)
_simConnectProvider.TurnOnTrackIR();
}
public void TurnOffTrackIR()
{
if (AppSettingData.AppSetting.AutoDisableTrackIR)
_simConnectProvider.TurnOffTrackIR();
}
public void TurnOnPower()
{
if (ProfileData.ActiveProfile != null)
_simConnectProvider.TurnOnPower(ProfileData.ActiveProfile.PowerOnRequiredForColdStart);
}
public void TurnOnAvionics()
{
if (ProfileData.ActiveProfile != null)
_simConnectProvider.TurnOnAvionics(ProfileData.ActiveProfile.PowerOnRequiredForColdStart);
}
public void TurnOffPower()
{
if (ProfileData.ActiveProfile != null)
_simConnectProvider.TurnOffpower(ProfileData.ActiveProfile.PowerOnRequiredForColdStart);
}
public void TurnOffAvionics()
{
if (ProfileData.ActiveProfile != null)
_simConnectProvider.TurnOffAvionics(ProfileData.ActiveProfile.PowerOnRequiredForColdStart);
}
public void AutoPanelPopOutActivation(bool activate)
{
if (activate)
_simConnectProvider.OnFlightStarted += HandleOnFlightStartedForAutoPopOut;
else
_simConnectProvider.OnFlightStarted -= HandleOnFlightStartedForAutoPopOut;
}
private void HandleOnFlightStartedForAutoPopOut(object sender, EventArgs e)
{
OnFlightStartedForAutoPopOut?.Invoke(this, null);
}
private void HandleOnFlightStopped(object sender, EventArgs e)
{
FlightSimData.IsEnteredFlight = false;
OnFlightStopped?.Invoke(this, null);
}
}
}

View file

@ -0,0 +1,137 @@
using AutoUpdaterDotNET;
using MSFSPopoutPanelManager.Shared;
using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
namespace MSFSPopoutPanelManager.Orchestration
{
public class MainOrchestrator : ObservableObject
{
public MainOrchestrator()
{
Profile = new ProfileOrchestrator();
PanelSource = new PanelSourceOrchestrator();
PanelPopOut = new PanelPopOutOrchestrator();
PanelConfiguration = new PanelConfigurationOrchestrator();
FlightSim = new FlightSimOrchestrator();
OnlineFeature = new OnlineFeatureOrchestrator();
TouchPanel = new TouchPanelOrchestrator();
FlightSimData = new FlightSimData();
FlightSimData.CurrentMsfsPlaneTitleChanged += (sernder, e) => ProfileData.RefreshProfile();
AppSettingData = new AppSettingData();
AppSettingData.AutoPopOutPanelsChanged += (sender, e) => FlightSim.AutoPanelPopOutActivation(e);
AppSettingData.TouchPanelIntegrationChanged += async (sender, e) =>
{
await TouchPanel.TouchPanelIntegrationUpdated(e);
};
ProfileData = new ProfileData();
ProfileData.FlightSimData = FlightSimData;
ProfileData.AppSettingData = AppSettingData;
ProfileData.ActiveProfileChanged += (sender, e) =>
{
PanelSource.CloseAllPanelSource();
};
}
public ProfileOrchestrator Profile { get; set; }
public PanelSourceOrchestrator PanelSource { get; set; }
public PanelPopOutOrchestrator PanelPopOut { get; set; }
public PanelConfigurationOrchestrator PanelConfiguration { get; set; }
public TouchPanelOrchestrator TouchPanel { get; set; }
public ProfileData ProfileData { get; set; }
public AppSettingData AppSettingData { get; private set; }
public FlightSimData FlightSimData { get; private set; }
public FlightSimOrchestrator FlightSim { get; set; }
public OnlineFeatureOrchestrator OnlineFeature { get; set; }
public IntPtr ApplicationHandle { get; set; }
public async void Initialize()
{
MigrateData.MigrateUserDataFiles();
AppSettingData.ReadSettings();
ProfileData.ReadProfiles();
Profile.ProfileData = ProfileData;
PanelSource.ProfileData = ProfileData;
PanelSource.AppSettingData = AppSettingData;
PanelSource.FlightSimData = FlightSimData;
PanelSource.FlightSimOrchestrator = FlightSim;
PanelPopOut.ProfileData = ProfileData;
PanelPopOut.AppSettingData = AppSettingData;
PanelPopOut.FlightSimData = FlightSimData;
PanelPopOut.FlightSimOrchestrator = FlightSim;
PanelPopOut.PanelSourceOrchestrator = PanelSource;
PanelPopOut.TouchPanelOrchestrator = TouchPanel;
PanelPopOut.OnPopOutCompleted += (sender, e) => TouchPanel.ReloadTouchPanelSimConnectDataDefinition();
PanelConfiguration.ProfileData = ProfileData;
PanelConfiguration.AppSettingData = AppSettingData;
FlightSim.ProfileData = ProfileData;
FlightSim.AppSettingData = AppSettingData;
FlightSim.FlightSimData = FlightSimData;
FlightSim.OnFlightStartedForAutoPopOut += (sender, e) => PanelPopOut.AutoPopOut();
TouchPanel.ProfileData = ProfileData;
TouchPanel.AppSettingData = AppSettingData;
TouchPanel.ApplicationHandle = ApplicationHandle;
CheckForAutoUpdate();
ProfileData.UpdateActiveProfile(AppSettingData.AppSetting.LastUsedProfileId); // Load last used profile
FlightSim.AutoPanelPopOutActivation(AppSettingData.AppSetting.AutoPopOutPanels); // Activate auto pop out panel if defined in preferences
FlightSim.StartSimConnectServer(); // Start the SimConnect server
// Enable/Disable touch panel feature (Personal use only feature)
try
{
var assembly = Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "WebServer.dll"));
if (assembly != null)
AppSettingData.AppSetting.IsEnabledTouchPanelServer = true;
if (AppSettingData.AppSetting.TouchPanelSettings.EnableTouchPanelIntegration)
await TouchPanel.StartServer();
assembly = null;
}
catch { }
}
public async Task ApplicationClose()
{
// Force unhook all win events
PanelConfiguration.EndConfiguration();
FlightSim.EndSimConnectServer(true);
await TouchPanel.StopServer();
}
private void CheckForAutoUpdate()
{
string jsonPath = Path.Combine(Path.Combine(FileIo.GetUserDataFilePath(), "autoupdate.json"));
AutoUpdater.PersistenceProvider = new JsonFilePersistenceProvider(jsonPath);
AutoUpdater.Synchronous = true;
AutoUpdater.AppTitle = "MSFS Pop Out Panel Manager";
AutoUpdater.RunUpdateAsAdmin = false;
AutoUpdater.UpdateFormSize = new System.Drawing.Size(930, 675);
AutoUpdater.Start(AppSettingData.AppSetting.AutoUpdaterUrl);
}
}
}

View file

@ -0,0 +1,133 @@
using MSFSPopoutPanelManager.Shared;
using System;
using System.IO;
namespace MSFSPopoutPanelManager.Orchestration
{
internal class MigrateData
{
public static void MigrateUserDataFiles()
{
var packageFile = new FileInfo("MSFSPopoutPanelManager.exe");
var newDataPath = Path.Combine(FileIo.GetUserDataFilePath());
var oldDataPath = Path.Combine(packageFile.DirectoryName, "userdata");
try
{
// Check if migration is necessary
if (new DirectoryInfo(newDataPath).Exists)
return;
// Check if upgrading from older version of app
if (!new DirectoryInfo(oldDataPath).Exists)
return;
const string APPSETTING_DATA_JSON = "appsettingdata.json";
const string USERPROFILE_DATA_JSON = "userprofiledata.json";
var installationPathDirInfo = packageFile.Directory;
var oldAppSettingDataJsonPath = Path.Combine(oldDataPath, APPSETTING_DATA_JSON);
var newAppSettingDataJsonPath = Path.Combine(newDataPath, APPSETTING_DATA_JSON);
var oldUserProfileDataJsonPath = Path.Combine(oldDataPath, USERPROFILE_DATA_JSON);
var newUserProfileDataJsonPath = Path.Combine(newDataPath, USERPROFILE_DATA_JSON);
StatusMessageWriter.WriteMessage($"Performing user data migration to your Windows 'Documents' folder. Please wait.....", StatusMessageType.Info, true, 3, true);
// Try to create user folder
if (!new DirectoryInfo(newDataPath).Exists)
{
var userDocumentFolder = Directory.CreateDirectory(newDataPath);
if (!new DirectoryInfo(newDataPath).Exists)
{
StatusMessageWriter.WriteMessage($"Unable to create folder '{userDocumentFolder}'. Application will close.", StatusMessageType.Error, true, 5, true);
Environment.Exit(0);
}
}
// Try move appsettingdata.json
if (new FileInfo(oldAppSettingDataJsonPath).Exists)
{
File.Copy(oldAppSettingDataJsonPath, newAppSettingDataJsonPath);
// Verify file has been copied
if (!new FileInfo(newAppSettingDataJsonPath).Exists)
{
StatusMessageWriter.WriteMessage("An error has occured when moving 'appsettingdata.json' to new folder location. Please try manually move this file and restart the app again.", StatusMessageType.Error, true, 5, true);
Environment.Exit(0);
}
File.Delete(oldAppSettingDataJsonPath);
}
// Try move userprofiledata.json
if (new FileInfo(oldUserProfileDataJsonPath).Exists)
{
File.Copy(oldUserProfileDataJsonPath, newUserProfileDataJsonPath);
// Verify file has been copied
if (!new FileInfo(newUserProfileDataJsonPath).Exists)
{
StatusMessageWriter.WriteMessage("An error has occured when moving 'userprofiledata.json' to new folder location. Please try manually move this file and restart the app again.", StatusMessageType.Error, true, 5, true);
Environment.Exit(0);
}
File.Delete(oldUserProfileDataJsonPath);
}
// Now remove all orphan files and folder
CleanFolderRecursive(installationPathDirInfo);
// Force an update of AppSetting file
var appSettingData = new AppSettingData();
appSettingData.ReadSettings();
appSettingData.AppSetting.AlwaysOnTop = false;
System.Threading.Thread.Sleep(500);
appSettingData.AppSetting.AlwaysOnTop = true;
StatusMessageWriter.WriteMessage(String.Empty, StatusMessageType.Info, false);
}
catch (Exception ex)
{
var msg = "An unknown user data migration error has occured. Application will close";
FileLogger.WriteException(msg, ex);
StatusMessageWriter.WriteMessage(msg, StatusMessageType.Error, true, 5, true);
Environment.Exit(0);
}
}
private static void CleanFolderRecursive(DirectoryInfo directory)
{
foreach (FileInfo filePath in directory.GetFiles())
{
var name = filePath.Name.ToLower();
if (name != "msfspopoutpanelmanager.exe")
{
try
{
filePath.Delete();
}
catch { }
}
}
foreach (DirectoryInfo dir in directory.GetDirectories())
{
CleanFolderRecursive(new DirectoryInfo(dir.FullName));
var name = dir.Name.ToLower();
if (name == "resources" || name == "userdata")
{
try
{
dir.Delete();
}
catch { }
}
}
}
}
}

View file

@ -0,0 +1,18 @@
using MSFSPopoutPanelManager.Shared;
using MSFSPopoutPanelManager.WindowsAgent;
namespace MSFSPopoutPanelManager.Orchestration
{
public class OnlineFeatureOrchestrator : ObservableObject
{
public void OpenUserGuide()
{
WindowProcessManager.OpenOnlineUserGuide();
}
public void OpenLatestDownload()
{
WindowProcessManager.OpenOnlineLatestDownload();
}
}
}

View file

@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<AssemblyName>Orchestration</AssemblyName>
<PackageId>MSFS 2020 Popout Panel Manager Orchestration</PackageId>
<Product>MSFS 2020 Popout Panel Manager Orchestration</Product>
<Authors>Stanley Kwok</Authors>
<Company>Stanley Kwok</Company>
<Copyright>Stanley Kwok 2021</Copyright>
<PackageProjectUrl>https://github.com/hawkeye-stan/msfs-popout-panel-manager</PackageProjectUrl>
<RootNamespace>MSFSPopoutPanelManager.Orchestration</RootNamespace>
<Platforms>x64</Platforms>
<Version>3.4.0.0</Version>
<AssemblyVersion>3.4.0.0</AssemblyVersion>
<FileVersion>3.4.0.0</FileVersion>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<DebugType>Embedded</DebugType>
<Configurations>Debug;Release;DebugTouchPanel;ReleaseTouchPanel</Configurations>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseTouchPanel|x64'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='DebugTouchPanel|x64'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autoupdater.NET.Official" Version="1.7.0" />
<PackageReference Include="Fody" Version="6.6.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="PropertyChanged.Fody" Version="3.4.1" />
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SimconnectAgent\SimconnectAgent.csproj" />
<ProjectReference Include="..\TouchPanelAgent\TouchPanelAgent.csproj" />
<ProjectReference Include="..\UserDataAgent\UserDataAgent.csproj" />
<ProjectReference Include="..\WebServer\WebServer.csproj" Condition=" '$(Configuration)' == 'ReleaseTouchPanel' Or '$(Configuration)' == 'DebugTouchPanel' " />
<ProjectReference Include="..\WindowsAgent\WindowsAgent.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,202 @@
using MSFSPopoutPanelManager.WindowsAgent;
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;
namespace MSFSPopoutPanelManager.Orchestration
{
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
public class PanelAnalyzer
{
public static Point GetMagnifyingGlassIconCoordinate(IntPtr hwnd)
{
var sourceImage = TakeScreenShot(hwnd);
if (sourceImage == null)
return new Point(0, 0);
Rectangle rectangle = WindowActionManager.GetClientRect(hwnd);
var panelMenubarTop = GetPanelMenubarTop(sourceImage, rectangle);
if (panelMenubarTop > sourceImage.Height)
return Point.Empty;
var panelMenubarBottom = GetPanelMenubarBottom(sourceImage, rectangle);
if (panelMenubarBottom > sourceImage.Height)
return Point.Empty;
var panelsStartingLeft = GetPanelMenubarStartingLeft(sourceImage, rectangle, panelMenubarTop + 5);
// The center of magnifying glass icon is around (3.2 x height of menubar) to the right of the panel menubar starting left
// But need to use higher number here to click the left side of magnifying glass icon because on some panel, the ratio is smaller
var menubarHeight = panelMenubarBottom - panelMenubarTop;
var magnifyingIconXCoor = panelsStartingLeft - Convert.ToInt32(menubarHeight * 3.2); // ToDo: play around with this multiplier to find the best for all resolutions
var magnifyingIconYCoor = panelMenubarTop + Convert.ToInt32(menubarHeight / 2);
return new Point(magnifyingIconXCoor, magnifyingIconYCoor);
}
private static Bitmap TakeScreenShot(IntPtr windowHandle)
{
if (!WindowActionManager.IsWindow(windowHandle))
return null;
// Set window to foreground so nothing can hide the window
PInvoke.SetForegroundWindow(windowHandle);
Thread.Sleep(300);
Rectangle rectangle = WindowActionManager.GetWindowRect(windowHandle);
Rectangle clientRectangle = WindowActionManager.GetClientRect(windowHandle);
// Take a screen shot by removing the titlebar of the window
var left = rectangle.Left;
var top = rectangle.Top + (rectangle.Height - clientRectangle.Height) - 8; // 8 pixels adjustments
var bmp = new Bitmap(clientRectangle.Width, clientRectangle.Height, PixelFormat.Format24bppRgb);
using (Graphics g = Graphics.FromImage(bmp))
{
g.CopyFromScreen(new Point(left, top), Point.Empty, rectangle.Size);
}
// Place the above image in the same canvas size as before
Bitmap backingImage = new Bitmap(rectangle.Width, rectangle.Height);
using (Graphics gfx = Graphics.FromImage(backingImage))
{
using (SolidBrush brush = new SolidBrush(Color.FromArgb(255, 0, 0)))
{
gfx.FillRectangle(brush, 0, 0, rectangle.Width, rectangle.Height);
gfx.DrawImage(bmp, new Point(0, top));
}
}
return backingImage;
}
private static int GetPanelMenubarTop(Bitmap sourceImage, Rectangle rectangle)
{
// Get a snippet of 1 pixel wide vertical strip of windows. We will choose the strip left of center.
// This is to determine when the actual panel's vertical pixel starts in the window. This will allow accurate sizing of the template image
var left = Convert.ToInt32((rectangle.Width) * 0.70); // look at around 70% from the left
var top = sourceImage.Height - rectangle.Height;
if (top < 0 || left < 0)
return -1;
unsafe
{
var stripData = sourceImage.LockBits(new Rectangle(left, top, 1, rectangle.Height), ImageLockMode.ReadWrite, sourceImage.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(stripData.PixelFormat) / 8;
int heightInPixels = stripData.Height;
int widthInBytes = stripData.Width * bytesPerPixel;
byte* ptrFirstPixel = (byte*)stripData.Scan0;
for (int y = 0; y < heightInPixels; y++)
{
byte* currentLine = ptrFirstPixel + (y * stripData.Stride);
for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
{
int red = currentLine[x + 2];
int green = currentLine[x + 1];
int blue = currentLine[x];
if (red == 255 && green == 255 && blue == 255)
{
sourceImage.UnlockBits(stripData);
return y + top;
}
}
}
sourceImage.UnlockBits(stripData);
}
return -1;
}
private static int GetPanelMenubarBottom(Bitmap sourceImage, Rectangle rectangle)
{
// Get a snippet of 1 pixel wide vertical strip of windows. We will choose the strip about 70% from the left of the window
var left = Convert.ToInt32((rectangle.Width) * 0.7); // look at around 70% from the left
var top = sourceImage.Height - rectangle.Height;
if (top < 0 || left < 0)
return -1;
unsafe
{
var stripData = sourceImage.LockBits(new Rectangle(left, top, 1, rectangle.Height), ImageLockMode.ReadWrite, sourceImage.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(stripData.PixelFormat) / 8;
int heightInPixels = stripData.Height;
int widthInBytes = stripData.Width * bytesPerPixel;
byte* ptrFirstPixel = (byte*)stripData.Scan0;
int menubarBottom = -1;
for (int y = 0; y < heightInPixels; y++)
{
byte* currentLine = ptrFirstPixel + (y * stripData.Stride);
for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
{
int red = currentLine[x + 2];
int green = currentLine[x + 1];
int blue = currentLine[x];
if (red > 250 && green > 250 && blue > 250) // allows the color to be a little off white (ie. Fenix A30 EFB)
{
// found the top of menu bar
menubarBottom = y + top;
}
else if (menubarBottom > -1) /// it is no longer white in color, we hit menubar bottom
{
sourceImage.UnlockBits(stripData);
return menubarBottom;
}
}
}
sourceImage.UnlockBits(stripData);
}
return -1;
}
private static int GetPanelMenubarStartingLeft(Bitmap sourceImage, Rectangle rectangle, int top)
{
unsafe
{
var stripData = sourceImage.LockBits(new Rectangle(0, top, rectangle.Width, 1), ImageLockMode.ReadWrite, sourceImage.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(stripData.PixelFormat) / 8;
int widthInPixels = stripData.Width;
int heightInBytes = stripData.Height * bytesPerPixel;
byte* ptrFirstPixel = (byte*)stripData.Scan0;
for (int x = 0; x < widthInPixels; x++)
{
byte* currentLine = ptrFirstPixel - (x * bytesPerPixel);
for (int y = 0; y < heightInBytes; y = y + bytesPerPixel)
{
int red = currentLine[y + 2];
int green = currentLine[y + 1];
int blue = currentLine[y];
if (red > 250 && green > 250 && blue > 250) // allows the color to be a little off white (ie. Fenix A30 EFB)
{
sourceImage.UnlockBits(stripData);
return sourceImage.Width - x;
}
}
}
sourceImage.UnlockBits(stripData);
}
return -1;
}
}
}

View file

@ -1,164 +1,181 @@
using MSFSPopoutPanelManager.Model; using MSFSPopoutPanelManager.Shared;
using MSFSPopoutPanelManager.UserDataAgent;
using MSFSPopoutPanelManager.WindowsAgent;
using System; using System;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace MSFSPopoutPanelManager.Provider namespace MSFSPopoutPanelManager.Orchestration
{ {
public class PanelConfigurationManager public class PanelConfigurationOrchestrator : ObservableObject
{ {
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 static PInvoke.WinEventProc _winEvent; // keep this as static to prevent garbage collect or the app will crash
private Rectangle _lastWindowRectangle; private static IntPtr _winEventHook;
private uint _prevWinEvent = PInvokeConstant.EVENT_SYSTEM_CAPTUREEND; private uint _prevWinEvent = PInvokeConstant.EVENT_SYSTEM_CAPTUREEND;
private int _winEventClickLock = 0; private int _winEventClickLock = 0;
private Rectangle _lastWindowRectangle;
private object _hookLock = new object();
private bool _isHookMouseDown = false;
public UserProfile UserProfile { get; set; } public PanelConfigurationOrchestrator()
public bool AllowEdit { get; set; }
public PanelConfigurationManager(UserProfileManager userProfileManager)
{ {
_userProfileManager = userProfileManager;
_winEvent = new PInvoke.WinEventProc(EventCallback); _winEvent = new PInvoke.WinEventProc(EventCallback);
AllowEdit = true; AllowEdit = true;
} }
public void HookWinEvent() internal ProfileData ProfileData { get; set; }
internal AppSettingData AppSettingData { get; set; }
private Profile ActiveProfile { get { return ProfileData == null ? null : ProfileData.ActiveProfile; } }
public bool AllowEdit { get; set; }
public void StartConfiguration()
{ {
// Setup panel config event hooks HookWinEvent();
_winEventHook = PInvoke.SetWinEventHook(PInvokeConstant.EVENT_SYSTEM_CAPTURESTART, PInvokeConstant.EVENT_OBJECT_LOCATIONCHANGE, DiagnosticManager.GetApplicationProcess().Handle, _winEvent, 0, 0, PInvokeConstant.WINEVENT_OUTOFCONTEXT);
} }
public void UnhookWinEvent() public void EndConfiguration()
{ {
// Unhook all Win API events UnhookWinEvent();
PInvoke.UnhookWinEvent(_winEventHook);
} }
public void LockPanelsUpdated() public void LockStatusUpdated()
{ {
UserProfile.IsLocked = !UserProfile.IsLocked; ActiveProfile.IsLocked = !ActiveProfile.IsLocked;
_userProfileManager.WriteUserProfiles(); ProfileData.WriteProfiles();
} }
public void PanelConfigPropertyUpdated(PanelConfigItem panelConfigItem) public void PanelConfigPropertyUpdated(int panelIndex, PanelConfigPropertyName configPropertyName)
{ {
if (panelConfigItem == null || !AllowEdit || UserProfile.IsLocked) if (panelIndex == -1 || !AllowEdit || ActiveProfile.IsLocked)
return; return;
var panelConfig = UserProfile.PanelConfigs.ToList().Find(p => p.PanelIndex == panelConfigItem.PanelIndex); var panelConfig = ActiveProfile.PanelConfigs.FirstOrDefault(p => p.PanelIndex == panelIndex);
if (panelConfig != null) if (panelConfig != null)
{ {
if (panelConfigItem.PanelConfigProperty == PanelConfigPropertyName.FullScreen) if (configPropertyName == PanelConfigPropertyName.FullScreen)
{ {
InputEmulationManager.ToggleFullScreenPanel(panelConfig.PanelHandle); InputEmulationManager.ToggleFullScreenPanel(panelConfig.PanelHandle);
panelConfig.HideTitlebar = false; panelConfig.HideTitlebar = false;
panelConfig.AlwaysOnTop = false; panelConfig.AlwaysOnTop = false;
} }
else if (panelConfigItem.PanelConfigProperty == PanelConfigPropertyName.PanelName) else if (configPropertyName == PanelConfigPropertyName.PanelName)
{ {
var name = panelConfig.PanelName; var name = panelConfig.PanelName;
if (name.IndexOf("(Custom)") == -1)
if (panelConfig.PanelType == PanelType.CustomPopout && name.IndexOf("(Custom)") == -1)
{
name = name + " (Custom)"; name = name + " (Custom)";
PInvoke.SetWindowText(panelConfig.PanelHandle, name); PInvoke.SetWindowText(panelConfig.PanelHandle, name);
}
} }
else if (!panelConfig.FullScreen) else if (!panelConfig.FullScreen)
{ {
switch (panelConfigItem.PanelConfigProperty) switch (configPropertyName)
{ {
case PanelConfigPropertyName.Left: case PanelConfigPropertyName.Left:
case PanelConfigPropertyName.Top: case PanelConfigPropertyName.Top:
WindowManager.MoveWindow(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); WindowActionManager.MoveWindow(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height);
break; break;
case PanelConfigPropertyName.Width: case PanelConfigPropertyName.Width:
case PanelConfigPropertyName.Height: case PanelConfigPropertyName.Height:
if (panelConfig.HideTitlebar) if (panelConfig.HideTitlebar)
WindowManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, false); WindowActionManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, false);
WindowManager.MoveWindowWithMsfsBugOverrirde(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); WindowActionManager.MoveWindowWithMsfsBugOverrirde(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height);
if (panelConfig.HideTitlebar) if (panelConfig.HideTitlebar)
WindowManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, true); WindowActionManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, true);
break; break;
case PanelConfigPropertyName.AlwaysOnTop: case PanelConfigPropertyName.AlwaysOnTop:
WindowManager.ApplyAlwaysOnTop(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.AlwaysOnTop, new Rectangle(panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height)); WindowActionManager.ApplyAlwaysOnTop(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.AlwaysOnTop, new Rectangle(panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height));
break; break;
case PanelConfigPropertyName.HideTitlebar: case PanelConfigPropertyName.HideTitlebar:
WindowManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, panelConfig.HideTitlebar); WindowActionManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, panelConfig.HideTitlebar);
break; break;
} }
} }
_userProfileManager.WriteUserProfiles(); ProfileData.WriteProfiles();
} }
} }
public void PanelConfigIncreaseDecrease(PanelConfigItem panelConfigItem, int changeAmount) public void PanelConfigIncreaseDecrease(int panelIndex, PanelConfigPropertyName configPropertyName, int changeAmount)
{ {
if (panelConfigItem == null || !AllowEdit || UserProfile.IsLocked || UserProfile.PanelConfigs == null || UserProfile.PanelConfigs.Count == 0) if (panelIndex == -1 || !AllowEdit || ActiveProfile.IsLocked || ActiveProfile.PanelConfigs == null || ActiveProfile.PanelConfigs.Count == 0)
return; return;
var index = UserProfile.PanelConfigs.ToList().FindIndex(p => p.PanelIndex == panelConfigItem.PanelIndex); var panelConfig = ActiveProfile.PanelConfigs.FirstOrDefault(p => p.PanelIndex == panelIndex);
if (index > -1) if (panelConfig != null)
{ {
var panelConfig = UserProfile.PanelConfigs[index]; // Should not apply any other settings if panel is full screen mode
// Cannot apply any other settings if panel is full screen mode
if (panelConfig.FullScreen) if (panelConfig.FullScreen)
return; return;
int orignalLeft = panelConfig.Left; int orignalLeft = panelConfig.Left;
switch (panelConfigItem.PanelConfigProperty) switch (configPropertyName)
{ {
case PanelConfigPropertyName.Left: case PanelConfigPropertyName.Left:
panelConfig.Left += changeAmount; panelConfig.Left += changeAmount;
WindowManager.MoveWindow(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); WindowActionManager.MoveWindow(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height);
break; break;
case PanelConfigPropertyName.Top: case PanelConfigPropertyName.Top:
panelConfig.Top += changeAmount; panelConfig.Top += changeAmount;
WindowManager.MoveWindow(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); WindowActionManager.MoveWindow(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height);
break; break;
case PanelConfigPropertyName.Width: case PanelConfigPropertyName.Width:
panelConfig.Width += changeAmount; panelConfig.Width += changeAmount;
if (panelConfig.HideTitlebar) if (panelConfig.HideTitlebar)
WindowManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, false); WindowActionManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, false);
WindowManager.MoveWindowWithMsfsBugOverrirde(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); WindowActionManager.MoveWindowWithMsfsBugOverrirde(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height);
if (panelConfig.HideTitlebar) if (panelConfig.HideTitlebar)
WindowManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, true); WindowActionManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, true);
break; break;
case PanelConfigPropertyName.Height: case PanelConfigPropertyName.Height:
panelConfig.Height += changeAmount; panelConfig.Height += changeAmount;
if (panelConfig.HideTitlebar) if (panelConfig.HideTitlebar)
WindowManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, false); WindowActionManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, false);
WindowManager.MoveWindowWithMsfsBugOverrirde(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height); WindowActionManager.MoveWindowWithMsfsBugOverrirde(panelConfig.PanelHandle, panelConfig.PanelType, panelConfig.Left, panelConfig.Top, panelConfig.Width, panelConfig.Height);
if (panelConfig.HideTitlebar) if (panelConfig.HideTitlebar)
WindowManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, true); WindowActionManager.ApplyHidePanelTitleBar(panelConfig.PanelHandle, true);
break; break;
default: default:
return; return;
} }
_userProfileManager.WriteUserProfiles(); ProfileData.WriteProfiles();
} }
} }
private void EventCallback(IntPtr hWinEventHook, uint iEvent, IntPtr hWnd, int idObject, int idChild, int dwEventThread, int dwmsEventTime) private void HookWinEvent()
{
// Setup panel config event hooks
_winEventHook = PInvoke.SetWinEventHook(PInvokeConstant.EVENT_SYSTEM_CAPTURESTART, PInvokeConstant.EVENT_OBJECT_LOCATIONCHANGE, IntPtr.Zero, _winEvent, 0, 0, PInvokeConstant.WINEVENT_OUTOFCONTEXT);
}
private void UnhookWinEvent()
{
// Unhook all Win API events
PInvoke.UnhookWinEvent(_winEventHook);
}
private void EventCallback(IntPtr hWinEventHook, uint iEvent, IntPtr hwnd, int idObject, int idChild, int dwEventThread, int dwmsEventTime)
{ {
switch (iEvent) switch (iEvent)
{ {
@ -167,31 +184,36 @@ namespace MSFSPopoutPanelManager.Provider
case PInvokeConstant.EVENT_SYSTEM_CAPTURESTART: case PInvokeConstant.EVENT_SYSTEM_CAPTURESTART:
case PInvokeConstant.EVENT_SYSTEM_CAPTUREEND: case PInvokeConstant.EVENT_SYSTEM_CAPTUREEND:
// check by priority to speed up comparing of escaping constraints // check by priority to speed up comparing of escaping constraints
if (hWnd == IntPtr.Zero if (hwnd == IntPtr.Zero
|| idObject != 0 || idObject != 0
|| hWinEventHook != _winEventHook || hWinEventHook != _winEventHook
|| !AllowEdit || !AllowEdit
|| UserProfile.PanelConfigs == null || ActiveProfile == null
|| UserProfile.PanelConfigs.Count == 0) || ActiveProfile.PanelConfigs == null
|| ActiveProfile.PanelConfigs.Count == 0)
{ {
return; return;
} }
HandleEventCallback(hWnd, iEvent); HandleEventCallback(hwnd, iEvent);
break; break;
default: default:
break; break;
} }
} }
private void HandleEventCallback(IntPtr hWnd, uint iEvent) private void HandleEventCallback(IntPtr hwnd, uint iEvent)
{ {
var panelConfig = UserProfile.PanelConfigs.FirstOrDefault(panel => panel.PanelHandle == hWnd); var panelConfig = ActiveProfile.PanelConfigs.FirstOrDefault(panel => panel.PanelHandle == hwnd);
if (panelConfig == null) if (panelConfig == null)
return; return;
if (panelConfig.IsLockable && UserProfile.IsLocked) // Should not apply any other settings if panel is full screen mode
if (panelConfig.FullScreen)
return;
if (panelConfig.IsLockable && ActiveProfile.IsLocked)
{ {
switch (iEvent) switch (iEvent)
{ {
@ -203,12 +225,18 @@ namespace MSFSPopoutPanelManager.Provider
// Detect if window is maximized, if so, save settings // Detect if window is maximized, if so, save settings
WINDOWPLACEMENT wp = new WINDOWPLACEMENT(); WINDOWPLACEMENT wp = new WINDOWPLACEMENT();
wp.length = System.Runtime.InteropServices.Marshal.SizeOf(wp); wp.length = System.Runtime.InteropServices.Marshal.SizeOf(wp);
PInvoke.GetWindowPlacement(hWnd, ref wp); PInvoke.GetWindowPlacement(hwnd, ref wp);
if (wp.showCmd == PInvokeConstant.SW_SHOWMAXIMIZED || wp.showCmd == PInvokeConstant.SW_SHOWMINIMIZED || wp.showCmd == PInvokeConstant.SW_SHOWNORMAL) if (wp.showCmd == PInvokeConstant.SW_SHOWMAXIMIZED || wp.showCmd == PInvokeConstant.SW_SHOWMINIMIZED || wp.showCmd == PInvokeConstant.SW_SHOWNORMAL)
{ {
PInvoke.ShowWindow(hWnd, PInvokeConstant.SW_RESTORE); PInvoke.ShowWindow(hwnd, PInvokeConstant.SW_RESTORE);
} }
break; break;
case PInvokeConstant.EVENT_SYSTEM_CAPTURESTART:
if (!panelConfig.HasTouchableEvent || _prevWinEvent == PInvokeConstant.EVENT_SYSTEM_CAPTURESTART)
break;
HandleTouchDownEvent(panelConfig);
break;
case PInvokeConstant.EVENT_SYSTEM_CAPTUREEND: case PInvokeConstant.EVENT_SYSTEM_CAPTUREEND:
if (!panelConfig.TouchEnabled || _prevWinEvent == PInvokeConstant.EVENT_OBJECT_LOCATIONCHANGE) if (!panelConfig.TouchEnabled || _prevWinEvent == PInvokeConstant.EVENT_OBJECT_LOCATIONCHANGE)
break; break;
@ -216,7 +244,7 @@ namespace MSFSPopoutPanelManager.Provider
if (!panelConfig.HasTouchableEvent || _prevWinEvent == PInvokeConstant.EVENT_SYSTEM_CAPTUREEND) if (!panelConfig.HasTouchableEvent || _prevWinEvent == PInvokeConstant.EVENT_SYSTEM_CAPTUREEND)
break; break;
HandleTouchEvent(panelConfig); HandleTouchUpEvent(panelConfig);
break; break;
} }
} }
@ -252,15 +280,21 @@ namespace MSFSPopoutPanelManager.Provider
// Detect if window is maximized, if so, save settings // Detect if window is maximized, if so, save settings
WINDOWPLACEMENT wp = new WINDOWPLACEMENT(); WINDOWPLACEMENT wp = new WINDOWPLACEMENT();
wp.length = System.Runtime.InteropServices.Marshal.SizeOf(wp); wp.length = System.Runtime.InteropServices.Marshal.SizeOf(wp);
PInvoke.GetWindowPlacement(hWnd, ref wp); PInvoke.GetWindowPlacement(hwnd, ref wp);
if (wp.showCmd == PInvokeConstant.SW_SHOWMAXIMIZED || wp.showCmd == PInvokeConstant.SW_SHOWMINIMIZED) if (wp.showCmd == PInvokeConstant.SW_SHOWMAXIMIZED || wp.showCmd == PInvokeConstant.SW_SHOWMINIMIZED)
{ {
_userProfileManager.WriteUserProfiles(); ProfileData.WriteProfiles();
} }
break; break;
case PInvokeConstant.EVENT_SYSTEM_MOVESIZEEND: case PInvokeConstant.EVENT_SYSTEM_MOVESIZEEND:
_userProfileManager.WriteUserProfiles(); ProfileData.WriteProfiles();
break;
case PInvokeConstant.EVENT_SYSTEM_CAPTURESTART:
if (!panelConfig.HasTouchableEvent || _prevWinEvent == PInvokeConstant.EVENT_SYSTEM_CAPTURESTART)
break;
HandleTouchDownEvent(panelConfig);
break; break;
case PInvokeConstant.EVENT_SYSTEM_CAPTUREEND: case PInvokeConstant.EVENT_SYSTEM_CAPTUREEND:
if (!panelConfig.TouchEnabled || _prevWinEvent == PInvokeConstant.EVENT_OBJECT_LOCATIONCHANGE) if (!panelConfig.TouchEnabled || _prevWinEvent == PInvokeConstant.EVENT_OBJECT_LOCATIONCHANGE)
@ -269,7 +303,7 @@ namespace MSFSPopoutPanelManager.Provider
if (!panelConfig.HasTouchableEvent || _prevWinEvent == PInvokeConstant.EVENT_SYSTEM_CAPTUREEND) if (!panelConfig.HasTouchableEvent || _prevWinEvent == PInvokeConstant.EVENT_SYSTEM_CAPTUREEND)
break; break;
HandleTouchEvent(panelConfig); HandleTouchUpEvent(panelConfig);
break; break;
} }
} }
@ -277,7 +311,27 @@ namespace MSFSPopoutPanelManager.Provider
_prevWinEvent = iEvent; _prevWinEvent = iEvent;
} }
private void HandleTouchEvent(PanelConfig panelConfig) private void HandleTouchDownEvent(PanelConfig panelConfig)
{
Point point;
PInvoke.GetCursorPos(out point);
// Disable left clicking if user is touching the title bar area
if (point.Y - panelConfig.Top > (panelConfig.HideTitlebar ? 5 : 31))
{
if (!_isHookMouseDown)
{
lock (_hookLock)
{
_isHookMouseDown = true;
InputEmulationManager.LeftClickMouseDown(point.X, point.Y);
}
}
}
}
private void HandleTouchUpEvent(PanelConfig panelConfig)
{ {
Point point; Point point;
PInvoke.GetCursorPos(out point); PInvoke.GetCursorPos(out point);
@ -287,11 +341,20 @@ namespace MSFSPopoutPanelManager.Provider
{ {
var prevWinEventClickLock = ++_winEventClickLock; var prevWinEventClickLock = ++_winEventClickLock;
UnhookWinEvent(); if (_isHookMouseDown)
InputEmulationManager.LeftClickFast(point.X, point.Y); {
HookWinEvent(); Thread.Sleep(AppSettingData.AppSetting.TouchScreenSettings.MouseUpDownDelay);
if (prevWinEventClickLock == _winEventClickLock) lock (_hookLock)
{
_isHookMouseDown = false;
UnhookWinEvent();
InputEmulationManager.LeftClickMouseUp(0, 0);
HookWinEvent();
}
}
if (prevWinEventClickLock == _winEventClickLock && AppSettingData.AppSetting.TouchScreenSettings.RefocusGameWindow)
{ {
Task.Run(() => RefocusMsfs(prevWinEventClickLock)); Task.Run(() => RefocusMsfs(prevWinEventClickLock));
} }
@ -304,11 +367,16 @@ namespace MSFSPopoutPanelManager.Provider
if (prevWinEventClickLock == _winEventClickLock) if (prevWinEventClickLock == _winEventClickLock)
{ {
var simulatorProcess = DiagnosticManager.GetSimulatorProcess(); var simulatorProcess = WindowProcessManager.GetSimulatorProcess();
Rectangle rectangle; Rectangle rectangle;
PInvoke.GetWindowRect(simulatorProcess.Handle, out rectangle); PInvoke.GetWindowRect(simulatorProcess.Handle, out rectangle);
PInvoke.SetCursorPos(rectangle.X + 18, rectangle.Y + 80);
if (!_isHookMouseDown)
{
PInvoke.SetCursorPos(rectangle.X + 18, rectangle.Y + 80);
}
} }
} }
} }

View file

@ -0,0 +1,452 @@
using MSFSPopoutPanelManager.Shared;
using MSFSPopoutPanelManager.UserDataAgent;
using MSFSPopoutPanelManager.WindowsAgent;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MSFSPopoutPanelManager.Orchestration
{
public class PanelPopOutOrchestrator : ObservableObject
{
// This will be replaced by a signal from Ready to Fly Skipper into webserver in version 4.0
private const int READY_TO_FLY_BUTTON_APPEARANCE_DELAY = 2000;
internal ProfileData ProfileData { get; set; }
internal AppSettingData AppSettingData { get; set; }
internal FlightSimData FlightSimData { get; set; }
internal FlightSimOrchestrator FlightSimOrchestrator { private get; set; }
internal PanelSourceOrchestrator PanelSourceOrchestrator { private get; set; }
internal TouchPanelOrchestrator TouchPanelOrchestrator { private get; set; }
private Profile ActiveProfile { get { return ProfileData == null ? null : ProfileData.ActiveProfile; } }
private AppSetting AppSetting { get { return AppSettingData == null ? null : AppSettingData.AppSetting; } }
public event EventHandler OnPopOutStarted;
public event EventHandler OnPopOutCompleted;
public event EventHandler<TouchPanelOpenEventArg> OnTouchPanelOpened;
public void ManualPopOut()
{
if (ActiveProfile == null)
return;
InputHookManager.EndHook();
if (ActiveProfile.PanelSourceCoordinates.Count > 0 || ActiveProfile.TouchPanelBindings.Count > 0)
{
StatusMessageWriter.WriteMessage($"Panels pop out in progress for profile:\n{ActiveProfile.ProfileName}", StatusMessageType.Info, true);
CorePopOutSteps();
}
}
public void AutoPopOut()
{
if (ActiveProfile == null)
return;
ProfileData.AutoSwitchProfile(FlightSimData.CurrentMsfsPlaneTitle);
FlightSimData.IsEnteredFlight = true;
// find the profile with the matching binding plane title
var profile = ProfileData.Profiles.FirstOrDefault(p => p.BindingAircraftLiveries.Any(p => p == FlightSimData.CurrentMsfsPlaneTitle));
// Do not do auto pop out if no profile matches the current livery
if (profile == null)
return;
// Match the delay for Ready to Fly button to disappear
Thread.Sleep(READY_TO_FLY_BUTTON_APPEARANCE_DELAY);
if (ActiveProfile.PanelSourceCoordinates.Count > 0 || ActiveProfile.TouchPanelBindings.Count > 0)
{
StatusMessageWriter.WriteMessage($"Automatic pop out is starting for profile:\n{profile.ProfileName}", StatusMessageType.Info, true);
// Extra wait for cockpit view to align
Thread.Sleep(1000);
CorePopOutSteps();
}
}
private void CorePopOutSteps()
{
// Has custom pop out panels
if (ActiveProfile.PanelSourceCoordinates.Count > 0)
{
// Turn off TrackIR if TrackIR is started
FlightSimOrchestrator.TurnOffTrackIR();
// Turn on power if required to pop out panels at least one (fix Cessna 208b grand caravan mod bug where battery is reported as on)
if (ActiveProfile.PowerOnRequiredForColdStart)
{
int count = 0;
do
{
FlightSimOrchestrator.TurnOnPower();
Thread.Sleep(500);
count++;
}
while (!FlightSimData.ElectricalMasterBatteryStatus && count < 10);
}
// Turn on avionics if required to pop out panels
if (ActiveProfile.PowerOnRequiredForColdStart)
FlightSimOrchestrator.TurnOnAvionics();
}
StartPopout();
// Has custom pop out panels
if (ActiveProfile.PanelSourceCoordinates.Count > 0)
{
// Turn off avionics if needed after pop out
FlightSimOrchestrator.TurnOffAvionics();
// Turn off power if needed after pop out
FlightSimOrchestrator.TurnOffPower();
// Return to custom camera view
ReturnToAfterPopOutCameraView();
// Turn TrackIR back on
FlightSimOrchestrator.TurnOnTrackIR();
}
}
private void StartPopout()
{
List<PanelConfig> panelConfigs = new List<PanelConfig>();
var simulatorProcess = WindowProcessManager.GetSimulatorProcess();
if (simulatorProcess == null || simulatorProcess.Handle == IntPtr.Zero)
{
StatusMessageWriter.WriteMessage("MSFS/SimConnect has not been started. Please try again at a later time.", StatusMessageType.Error, false);
return;
}
if (ActiveProfile == null)
{
StatusMessageWriter.WriteMessage("No profile has been selected. Please select a profile to continue.", StatusMessageType.Error, false);
return;
}
if (ActiveProfile.PanelSourceCoordinates.Count == 0 && ActiveProfile.TouchPanelBindings.Count == 0)
{
StatusMessageWriter.WriteMessage("No panel has been selected for the profile. Please select at least one panel to continue.", StatusMessageType.Error, false);
return;
}
// Close all existing custom pop out panels
WindowActionManager.CloseAllPopOuts();
// Close all panel source overlays
PanelSourceOrchestrator.CloseAllPanelSource();
OnPopOutStarted?.Invoke(this, null);
// Must close out all existing custom pop out panels
if (WindowActionManager.GetWindowsCountByPanelType(new List<PanelType>() { PanelType.CustomPopout, PanelType.MSFSTouchPanel }) > 0)
{
StatusMessageWriter.WriteMessage("Please close all existing panel pop outs to continue.", StatusMessageType.Error, false);
return;
}
// Try to pop out and separate custom panels
if (ActiveProfile.PanelSourceCoordinates.Count > 0)
{
if (AppSetting.UseAutoPanning)
InputEmulationManager.LoadCustomView(AppSetting.AutoPanningKeyBinding);
var panelResults = ExecutePopoutAndSeparation();
if (panelResults == null)
return;
panelConfigs.AddRange(panelResults);
}
// Add the MSFS Touch Panel (My other github project) windows to the panel list
if (AppSetting.TouchPanelSettings.EnableTouchPanelIntegration)
{
var panelResults = AddMsfsTouchPanels(panelConfigs.Count + 1);
if (panelResults == null)
return;
panelConfigs.AddRange(panelResults);
}
if (panelConfigs.Count == 0)
{
OnPopOutCompleted?.Invoke(this, null);
StatusMessageWriter.WriteMessage("No panels have been found. Please select at least one in-game panel.", StatusMessageType.Error, false);
return;
}
// Line up all the panels
for (var i = panelConfigs.Count - 1; i >= 0; i--)
{
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);
PInvoke.SetForegroundWindow(panelConfigs[i].PanelHandle);
Thread.Sleep(200);
}
}
if (panelConfigs.Count > 0 || ActiveProfile.PanelConfigs.Count > 0)
{
LoadAndApplyPanelConfigs(panelConfigs);
ActiveProfile.PanelConfigs = new ObservableCollection<PanelConfig>(panelConfigs);
StatusMessageWriter.WriteMessage("Panels have been popped out succesfully and saved panel settings have been applied.", StatusMessageType.Info, true);
OnPopOutCompleted?.Invoke(this, null);
}
else
{
ActiveProfile.PanelConfigs = new ObservableCollection<PanelConfig>(panelConfigs);
StatusMessageWriter.WriteMessage("Panels have been popped out succesfully.", StatusMessageType.Info, true);
OnPopOutCompleted?.Invoke(this, null);
}
}
private List<PanelConfig> ExecutePopoutAndSeparation()
{
List<PanelConfig> panels = new List<PanelConfig>();
// PanelIndex starts at 1
for (var i = 1; i <= ActiveProfile.PanelSourceCoordinates.Count; i++)
{
var x = ActiveProfile.PanelSourceCoordinates[i - 1].X;
var y = ActiveProfile.PanelSourceCoordinates[i - 1].Y;
InputEmulationManager.PopOutPanel(x, y, AppSetting.UseLeftRightControlToPopOut);
var handle = PInvoke.FindWindow("AceApp", String.Empty); // Get an AceApp window with empty title
// 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);
if (i > 1)
SeparatePanel(panels[0].PanelHandle); // The joined panel is always the first panel that got popped out
handle = PInvoke.FindWindow("AceApp", String.Empty); // Get an AceApp window with empty title
if (handle == IntPtr.Zero && i == 1)
{
StatusMessageWriter.WriteMessage("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.", StatusMessageType.Error, true);
return null;
}
else if (handle == IntPtr.Zero)
{
StatusMessageWriter.WriteMessage($"Unable to pop out panel number {i}. Please check panel's number circle is positioned inside the panel, check for panel obstruction, and check if panel can be popped out. Pop out process stopped.", StatusMessageType.Error, true);
return null;
}
var panel = new PanelConfig();
panel.PanelHandle = handle;
panel.PanelType = PanelType.CustomPopout;
panel.PanelIndex = i;
panel.PanelName = $"Panel{i}";
panel.Top = (i - 1) * 30;
panel.Left = (i - 1) * 30;
panel.Width = 800;
panel.Height = 600;
panels.Add(panel);
PInvoke.SetWindowText(panel.PanelHandle, panel.PanelName + " (Custom)");
}
//Performance validation, make sure the number of pop out panels is equal to the number of selected panel
if (WindowActionManager.GetWindowsCountByPanelType(new List<PanelType>() { PanelType.CustomPopout }) != ActiveProfile.PanelSourceCoordinates.Count)
{
StatusMessageWriter.WriteMessage("Unable to pop out all panels. Please align all panel number circles with in-game panel locations.", StatusMessageType.Error, false);
return null;
}
return panels;
}
private void SeparatePanel(IntPtr hwnd)
{
// Resize all windows to 800x600 when separating and shimmy the panel
// 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);
PInvoke.SetForegroundWindow(hwnd);
Thread.Sleep(500);
// Find the magnifying glass coordinate
var point = PanelAnalyzer.GetMagnifyingGlassIconCoordinate(hwnd);
InputEmulationManager.LeftClick(point.X, point.Y);
}
private List<PanelConfig> AddMsfsTouchPanels(int panelIndex)
{
List<PanelConfig> touchPanels = new List<PanelConfig>();
if (AppSetting.TouchPanelSettings.EnableTouchPanelIntegration)
{
TouchPanelOrchestrator.LoadPlaneProfiles();
if (TouchPanelOrchestrator.PlaneProfiles == null)
return null;
// Find all selected panels
var panelConfigs = TouchPanelOrchestrator.PlaneProfiles.SelectMany(p => p.Panels.Where(c => c.IsSelected));
foreach (var panelConfig in panelConfigs)
{
var caption = $"{panelConfig.Name} (Touch Panel)";
// Change width and height to 1080p aspect ratio
double aspectRatio = 1;
if (panelConfig.Width > 1920)
{
aspectRatio = Convert.ToDouble(1920) / panelConfig.Width;
panelConfig.Width = 1920; // there are hidden padding to make it to 1920
panelConfig.Height = Convert.ToInt32(panelConfig.Height * aspectRatio);
}
OnTouchPanelOpened?.Invoke(this, new TouchPanelOpenEventArg() { PlaneId = panelConfig.PlaneId, PanelId = panelConfig.PanelId, Caption = caption, Width = panelConfig.Width, Height = panelConfig.Height });
// detect for a max of 5 seconds
int tryCount = 0;
while (tryCount < 10)
{
var touchPanelHandle = WindowActionManager.FindWindowByCaption(caption);
if (touchPanelHandle != IntPtr.Zero)
{
var dimension = WindowActionManager.GetWindowRect(touchPanelHandle);
var panelInfo = new PanelConfig
{
PanelIndex = panelIndex,
PanelHandle = touchPanelHandle,
PanelName = caption,
PanelType = PanelType.MSFSTouchPanel,
Top = dimension.Top,
Left = dimension.Left,
Width = panelConfig.Width,
Height = panelConfig.Height,
AlwaysOnTop = true // default to always on top
};
touchPanels.Add(panelInfo);
break;
}
Thread.Sleep(500);
tryCount++;
}
if (tryCount == 10)
return null;
panelIndex++;
}
}
return touchPanels;
}
private void LoadAndApplyPanelConfigs(List<PanelConfig> panelResults)
{
Parallel.ForEach(panelResults, panel =>
{
// Something is wrong here where panel has no window handle
if (panel.PanelHandle == IntPtr.Zero)
return;
var savedPanelConfig = ActiveProfile.PanelConfigs.FirstOrDefault(s => s.PanelIndex == panel.PanelIndex);
// Assign previous saved values
if (savedPanelConfig != null)
{
panel.PanelName = savedPanelConfig.PanelName;
panel.Top = savedPanelConfig.Top;
panel.Left = savedPanelConfig.Left;
panel.Width = savedPanelConfig.Width;
panel.Height = savedPanelConfig.Height;
panel.FullScreen = savedPanelConfig.FullScreen;
panel.AlwaysOnTop = savedPanelConfig.AlwaysOnTop;
panel.HideTitlebar = savedPanelConfig.HideTitlebar;
panel.TouchEnabled = savedPanelConfig.TouchEnabled;
}
// Apply panel name
if (panel.PanelType == PanelType.CustomPopout)
{
var caption = panel.PanelName + " (Custom)";
PInvoke.SetWindowText(panel.PanelHandle, caption);
Thread.Sleep(500);
}
// Apply locations
if (panel.Width != 0 && panel.Height != 0)
{
PInvoke.ShowWindow(panel.PanelHandle, PInvokeConstant.SW_RESTORE);
Thread.Sleep(250);
WindowActionManager.MoveWindow(panel.PanelHandle, panel.PanelType, panel.Left, panel.Top, panel.Width, panel.Height);
Thread.Sleep(1000);
}
if (!panel.FullScreen)
{
// Apply always on top
if (panel.AlwaysOnTop)
{
WindowActionManager.ApplyAlwaysOnTop(panel.PanelHandle, panel.PanelType, true, new Rectangle(panel.Left, panel.Top, panel.Width, panel.Height));
Thread.Sleep(1000);
}
// Apply hide title bar
if (panel.HideTitlebar)
WindowActionManager.ApplyHidePanelTitleBar(panel.PanelHandle, true);
}
PInvoke.ShowWindow(panel.PanelHandle, PInvokeConstant.SW_RESTORE);
});
// Apply full screen (cannot combine with always on top or hide title bar)
// Cannot run in parallel process
panelResults.ForEach(panel =>
{
if (panel.FullScreen && (!panel.AlwaysOnTop && !panel.HideTitlebar))
{
InputEmulationManager.ToggleFullScreenPanel(panel.PanelHandle);
Thread.Sleep(250);
}
});
}
private void ReturnToAfterPopOutCameraView()
{
if (!AppSetting.AfterPopOutCameraView.EnableReturnToCameraView)
return;
switch (AppSetting.AfterPopOutCameraView.CameraView)
{
case AfterPopOutCameraViewType.CockpitCenterView:
InputEmulationManager.CenterView();
break;
case AfterPopOutCameraViewType.CustomCameraView:
InputEmulationManager.LoadCustomView(AppSetting.AfterPopOutCameraView.CustomCameraKeyBinding);
break;
}
}
}
}

View file

@ -0,0 +1,173 @@
using MSFSPopoutPanelManager.Shared;
using MSFSPopoutPanelManager.UserDataAgent;
using MSFSPopoutPanelManager.WindowsAgent;
using System;
using System.Collections.ObjectModel;
using System.Drawing;
namespace MSFSPopoutPanelManager.Orchestration
{
public class PanelSourceOrchestrator : ObservableObject
{
private int _panelIndex;
internal ProfileData ProfileData { get; set; }
internal AppSettingData AppSettingData { get; set; }
internal FlightSimData FlightSimData { get; set; }
internal FlightSimOrchestrator FlightSimOrchestrator { get; set; }
private Profile ActiveProfile { get { return ProfileData == null ? null : ProfileData.ActiveProfile; } }
private AppSetting AppSetting { get { return AppSettingData == null ? null : AppSettingData.AppSetting; } }
public event EventHandler<PanelSourceCoordinate> onOverlayShowed;
public event EventHandler OnLastOverlayRemoved;
public event EventHandler OnAllOverlaysRemoved;
public event EventHandler OnSelectionStarted;
public event EventHandler OnSelectionCompleted;
public bool IsEditingPanelSource { get; set; }
public void StartPanelSelection()
{
if (ActiveProfile == null)
return;
_panelIndex = 1;
// remove all existing panel overlay display
for (var i = 0; i < ActiveProfile.PanelSourceCoordinates.Count; i++)
OnLastOverlayRemoved?.Invoke(this, null);
ActiveProfile.PanelSourceCoordinates = new ObservableCollection<PanelSourceCoordinate>();
ActiveProfile.PanelConfigs.Clear();
ActiveProfile.IsLocked = false;
InputHookManager.OnLeftClick -= HandleOnPanelSelectionAdded;
InputHookManager.OnLeftClick += HandleOnPanelSelectionAdded;
InputHookManager.OnShiftLeftClick -= HandleOnLastPanelSelectionRemoved;
InputHookManager.OnShiftLeftClick += HandleOnLastPanelSelectionRemoved;
InputHookManager.OnCtrlLeftClick -= HandleOnPanelSelectionCompleted;
InputHookManager.OnCtrlLeftClick += HandleOnPanelSelectionCompleted;
// Turn off TrackIR if TrackIR is started
FlightSimOrchestrator.TurnOffTrackIR();
OnSelectionStarted?.Invoke(this, null);
InputHookManager.StartHook();
}
public void SaveAutoPanningCamera()
{
var simualatorProcess = WindowProcessManager.GetSimulatorProcess();
if (simualatorProcess == null)
{
StatusMessageWriter.WriteMessage("MSFS/SimConnect has not been started. Please try again at a later time.", StatusMessageType.Error, false);
return;
}
InputEmulationManager.SaveCustomView(AppSettingData.AppSetting.AutoPanningKeyBinding);
StatusMessageWriter.WriteMessage("Auto Panning Camera has been saved succesfully.", StatusMessageType.Info, false);
}
public void EditPanelSource()
{
IsEditingPanelSource = !IsEditingPanelSource;
if (IsEditingPanelSource)
StartEditPanelSource();
else
EndEditPanelSource();
}
public void CloseAllPanelSource()
{
IsEditingPanelSource = false;
OnAllOverlaysRemoved?.Invoke(this, null);
}
public void HandleOnPanelSelectionAdded(object sender, Point e)
{
if (ActiveProfile == null)
return;
var newCoor = new PanelSourceCoordinate() { PanelIndex = _panelIndex, X = e.X, Y = e.Y };
ActiveProfile.PanelSourceCoordinates.Add(newCoor);
_panelIndex++;
onOverlayShowed?.Invoke(this, newCoor);
}
public void HandleOnLastPanelSelectionRemoved(object sender, Point e)
{
if (ActiveProfile == null)
return;
if (ActiveProfile.PanelSourceCoordinates.Count > 0)
{
ActiveProfile.PanelSourceCoordinates.RemoveAt(ActiveProfile.PanelSourceCoordinates.Count - 1);
_panelIndex--;
OnLastOverlayRemoved?.Invoke(this, null);
}
}
private void StartEditPanelSource()
{
if (ActiveProfile == null)
return;
// remove all existing panel overlay display
for (var i = 0; i < ActiveProfile.PanelSourceCoordinates.Count; i++)
OnAllOverlaysRemoved?.Invoke(this, null);
foreach (var coor in ActiveProfile.PanelSourceCoordinates)
onOverlayShowed?.Invoke(this, new PanelSourceCoordinate() { PanelIndex = coor.PanelIndex, X = coor.X, Y = coor.Y });
// Turn off TrackIR if TrackIR is started
FlightSimOrchestrator.TurnOffTrackIR();
if (AppSetting.UseAutoPanning && FlightSimData.IsEnteredFlight)
InputEmulationManager.LoadCustomView(AppSetting.AutoPanningKeyBinding);
}
private void EndEditPanelSource()
{
if (ActiveProfile == null)
return;
// remove all existing panel overlay display
for (var i = 0; i < ActiveProfile.PanelSourceCoordinates.Count; i++)
OnAllOverlaysRemoved?.Invoke(this, null);
// Turn TrackIR back on
FlightSimOrchestrator.TurnOnTrackIR();
}
private void HandleOnPanelSelectionCompleted(object sender, Point e)
{
if (ActiveProfile == null)
return;
//If enable, save the current viewport into custom view by Ctrl-Alt-0
if (AppSetting.UseAutoPanning)
InputEmulationManager.SaveCustomView(AppSetting.AutoPanningKeyBinding);
ProfileData.WriteProfiles();
InputHookManager.EndHook();
// Turn TrackIR back on
FlightSimOrchestrator.TurnOnTrackIR();
IsEditingPanelSource = false;
OnSelectionCompleted?.Invoke(this, null);
}
}
}

View file

@ -0,0 +1,169 @@
using MSFSPopoutPanelManager.Shared;
using MSFSPopoutPanelManager.UserDataAgent;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
namespace MSFSPopoutPanelManager.Orchestration
{
public class ProfileData : ObservableObject
{
public event PropertyChangedEventHandler ActiveProfileChanged;
public ProfileData()
{
Profiles = new ObservableCollection<Profile>();
}
public ObservableCollection<Profile> Profiles { get; private set; }
public FlightSimData FlightSimData { private get; set; }
public AppSettingData AppSettingData { private get; set; }
public int AddProfile(string profileName)
{
var newProfileId = ProfileManager.AddProfile(profileName, Profiles);
UpdateActiveProfile(newProfileId);
AppSettingData.AppSetting.LastUsedProfileId = newProfileId;
return newProfileId;
}
public int AddProfile(string profileName, int copyFromProfileId)
{
var newProfileId = ProfileManager.AddProfile(profileName, copyFromProfileId, Profiles);
UpdateActiveProfile(newProfileId);
AppSettingData.AppSetting.LastUsedProfileId = newProfileId;
return newProfileId;
}
public bool DeleteProfile(int profileId)
{
if (ActiveProfile == null)
return false;
return ProfileManager.DeleteProfile(profileId, Profiles);
}
public void AddProfileBinding(string planeTitle, int activeProfileId)
{
if (ActiveProfile == null)
return;
ProfileManager.AddProfileBinding(planeTitle, activeProfileId, Profiles);
RefreshProfile();
}
public void DeleteProfileBinding(string planeTitle, int activeProfileId)
{
if (ActiveProfile == null)
return;
ProfileManager.DeleteProfileBinding(planeTitle, activeProfileId, Profiles);
RefreshProfile();
}
public void ReadProfiles()
{
Profiles = new ObservableCollection<Profile>(ProfileManager.ReadProfiles());
}
public void WriteProfiles()
{
ProfileManager.WriteProfiles(Profiles);
}
public void UpdateActiveProfile(int profileId)
{
if (profileId == -1 && Profiles.Count > 0)
ActiveProfile = Profiles.FirstOrDefault(p => p.ProfileId == Profiles[0].ProfileId);
else if (profileId == -1 || Profiles.Count == 0)
ActiveProfile = null;
else
ActiveProfile = Profiles.FirstOrDefault(p => p.ProfileId == profileId);
// Set active profile flag, this is used only for MVVM binding
Profiles.ToList().ForEach(p => p.IsActive = false);
if (ActiveProfile != null)
{
ActiveProfile.IsActive = true;
AppSettingData.AppSetting.LastUsedProfileId = ActiveProfile.ProfileId;
}
else
{
AppSettingData.AppSetting.LastUsedProfileId = -1;
}
ActiveProfileChanged?.Invoke(this, null);
}
public Profile ActiveProfile { get; private set; }
public bool HasActiveProfile { get { return ActiveProfile != null; } }
public bool IsAircraftBoundToProfile
{
get
{
if (ActiveProfile == null)
return false;
return ActiveProfile.BindingAircraftLiveries.Any(p => p == FlightSimData.CurrentMsfsPlaneTitle);
}
}
public bool IsAllowedDeleteAircraftBinding
{
get
{
if (ActiveProfile == null || !FlightSimData.HasCurrentMsfsPlaneTitle)
return false;
var uProfile = Profiles.FirstOrDefault(u => u.BindingAircraftLiveries.Any(p => p == FlightSimData.CurrentMsfsPlaneTitle));
if (uProfile != null && uProfile.ProfileId != ActiveProfile.ProfileId)
return false;
return ActiveProfile.BindingAircraftLiveries.Any(p => p == FlightSimData.CurrentMsfsPlaneTitle);
}
}
public bool IsAllowedAddAircraftBinding
{
get
{
if (ActiveProfile == null || !FlightSimData.HasCurrentMsfsPlaneTitle)
return false;
var uProfile = Profiles.FirstOrDefault(u => u.BindingAircraftLiveries.Any(p => p == FlightSimData.CurrentMsfsPlaneTitle));
if (uProfile != null && uProfile.ProfileId != ActiveProfile.ProfileId)
return false;
return !ActiveProfile.BindingAircraftLiveries.Any(p => p == FlightSimData.CurrentMsfsPlaneTitle);
}
}
public void RefreshProfile()
{
int currentProfileId;
if (ActiveProfile == null)
currentProfileId = -1;
else
currentProfileId = ActiveProfile.ProfileId;
ActiveProfile = null;
UpdateActiveProfile(currentProfileId);
}
public void AutoSwitchProfile(string activeAircraftTitle)
{
// Automatic switching of active profile when SimConnect active aircraft livery changes
if (Profiles != null)
{
var matchedProfile = Profiles.FirstOrDefault(p => p.BindingAircraftLiveries.Any(t => t == activeAircraftTitle));
if (matchedProfile != null)
UpdateActiveProfile(matchedProfile.ProfileId);
}
}
}
}

View file

@ -0,0 +1,41 @@
using MSFSPopoutPanelManager.Shared;
namespace MSFSPopoutPanelManager.Orchestration
{
public class ProfileOrchestrator : ObservableObject
{
internal ProfileData ProfileData { get; set; }
public void AddProfile(string profileName, int copyProfileId)
{
if (copyProfileId == -1)
ProfileData.AddProfile(profileName);
else
ProfileData.AddProfile(profileName, copyProfileId);
}
public void DeleteActiveProfile()
{
if (ProfileData.ActiveProfile != null)
ProfileData.DeleteProfile(ProfileData.ActiveProfile.ProfileId);
}
public void ChangeProfile(int profileId)
{
if (ProfileData != null)
ProfileData.UpdateActiveProfile(profileId);
}
public void AddProfileBinding(string bindingLiveryName)
{
if (ProfileData.ActiveProfile != null)
ProfileData.AddProfileBinding(bindingLiveryName, ProfileData.ActiveProfile.ProfileId);
}
public void DeleteProfileBinding(string bindingLiveryName)
{
if (ProfileData.ActiveProfile != null)
ProfileData.DeleteProfileBinding(bindingLiveryName, ProfileData.ActiveProfile.ProfileId);
}
}
}

View file

@ -0,0 +1,187 @@
using MSFSPopoutPanelManager.Shared;
using MSFSPopoutPanelManager.TouchPanelAgent;
using MSFSPopoutPanelManager.UserDataAgent;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Dynamic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace MSFSPopoutPanelManager.Orchestration
{
public class TouchPanelOrchestrator : ObservableObject
{
private ITouchPanelServer _touchPanelServer;
internal ProfileData ProfileData { get; set; }
internal AppSettingData AppSettingData { get; set; }
public ObservableCollection<PlaneProfile> PlaneProfiles { get; private set; }
public IntPtr ApplicationHandle { get; set; }
public void LoadPlaneProfiles()
{
if (_touchPanelServer == null || ProfileData.ActiveProfile == null)
return;
var dataItems = TouchPanelManager.GetPlaneProfiles();
// Map plane profiles to bindable objects
PlaneProfiles = new ObservableCollection<PlaneProfile>();
foreach (var dataItem in dataItems)
{
var planeProfile = new PlaneProfile();
planeProfile.PlaneId = dataItem.PlaneId;
planeProfile.Name = dataItem.Name;
if (dataItem.Panels != null && dataItem.Panels.Count > 0)
{
planeProfile.Panels = new ObservableCollection<PanelProfile>();
foreach (var dataItemPanel in dataItem.Panels)
{
planeProfile.Panels.Add(new PanelProfile()
{
PlaneId = dataItem.PlaneId,
PanelId = dataItemPanel.PanelId,
Name = dataItemPanel.Name,
Width = dataItemPanel.PanelSize.Width,
Height = dataItemPanel.PanelSize.Height,
IsSelected = ProfileData.ActiveProfile.TouchPanelBindings == null ? false : ProfileData.ActiveProfile.TouchPanelBindings.Any(b => b.PlaneId == dataItem.PlaneId && b.PanelId == dataItemPanel.PanelId)
});
}
}
PlaneProfiles.Add(planeProfile);
}
}
public void PanelSelected(IList<PlaneProfile> PlaneProfiles)
{
ProfileData.ActiveProfile.PanelConfigs.RemoveAll(panelConfig => panelConfig.PanelType == PanelType.MSFSTouchPanel);
ProfileData.ActiveProfile.TouchPanelBindings.Clear();
ProfileData.ActiveProfile.IsLocked = false;
foreach (var planeProfile in PlaneProfiles)
{
foreach (var panel in planeProfile.Panels)
{
if (panel.IsSelected)
ProfileData.ActiveProfile.TouchPanelBindings.Add(new TouchPanelBinding() { PlaneId = planeProfile.PlaneId, PanelId = panel.PanelId });
}
}
ProfileData.WriteProfiles();
}
internal async Task StartServer()
{
if (_touchPanelServer == null)
{
// This is use to remove all references and dependencies so single file package will not include any ASP.NET core dlls
#if DEBUGTOUCHPANEL || RELEASETOUCHPANEL
_touchPanelServer = new MSFSPopoutPanelManager.WebServer.WebHost();
#endif
if (_touchPanelServer != null)
{
_touchPanelServer.WindowHandle = ApplicationHandle;
_touchPanelServer.DataRefreshInterval = AppSettingData.AppSetting.TouchPanelSettings.DataRefreshInterval;
_touchPanelServer.MapRefreshInterval = AppSettingData.AppSetting.TouchPanelSettings.MapRefreshInterval;
_touchPanelServer.IsUsedArduino = AppSettingData.AppSetting.TouchPanelSettings.UseArduino;
_touchPanelServer.IsEnabledSound = AppSettingData.AppSetting.TouchPanelSettings.EnableSound;
await _touchPanelServer.StartAsync(default(CancellationToken));
}
}
}
internal async Task StopServer()
{
if (_touchPanelServer != null && _touchPanelServer.ServerStarted)
{
await _touchPanelServer.StopAsync(default(CancellationToken));
_touchPanelServer = null;
}
}
internal async Task TouchPanelIntegrationUpdated(bool enabled)
{
if ((_touchPanelServer == null || !_touchPanelServer.ServerStarted) && enabled)
await StartServer();
else if (_touchPanelServer.ServerStarted && !enabled)
await StopServer();
}
internal async void ReloadTouchPanelSimConnectDataDefinition()
{
if (_touchPanelServer == null)
return;
// Communicate with Touch Panel API server to reload SimConnect data definitions
if (ProfileData.ActiveProfile != null && ProfileData.ActiveProfile.TouchPanelBindings.Count > 0 && _touchPanelServer.ServerStarted)
{
var planeId = ProfileData.ActiveProfile.TouchPanelBindings.Count > 0 ?
ProfileData.ActiveProfile.TouchPanelBindings[0].PlaneId :
null;
using (var client = new HttpClient())
{
dynamic data = new ExpandoObject();
data.PlaneId = planeId;
var payload = JsonConvert.SerializeObject(data);
var url = "http://localhost:27012/posttouchpanelloaded";
try
{
var response = await client.PostAsync(url, new StringContent(payload, Encoding.UTF8, "application/json"));
var token = response.Content.ReadAsStringAsync().Result;
}
catch { }
}
}
}
}
public class PlaneProfile : ObservableObject
{
public string PlaneId { get; set; }
public string Name { get; set; }
public ObservableCollection<PanelProfile> Panels { get; set; }
public bool HasSelected
{
get
{
if (Panels == null)
return false;
return Panels.Any(p => p.IsSelected);
}
}
}
public class PanelProfile : ObservableObject
{
public string PlaneId { get; set; }
public string PanelId { get; set; }
public string Name { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public bool IsSelected { get; set; }
}
}

View file

@ -1,51 +0,0 @@
using MSFSPopoutPanelManager.Shared;
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;
namespace MSFSPopoutPanelManager.Provider
{
public class ImageOperation
{
public static Bitmap TakeScreenShot(IntPtr windowHandle)
{
if (!PInvoke.IsWindow(windowHandle))
throw new PopoutManagerException("Pop out windows were closed unexpectedly.");
// Set window to foreground so nothing can hide the window
PInvoke.SetForegroundWindow(windowHandle);
Thread.Sleep(300);
Rectangle rectangle;
PInvoke.GetWindowRect(windowHandle, out rectangle);
Rectangle clientRectangle;
PInvoke.GetClientRect(windowHandle, out clientRectangle);
// Take a screen shot by removing the titlebar of the window
var left = rectangle.Left;
var top = rectangle.Top + (rectangle.Height - clientRectangle.Height) - 8; // 8 pixels adjustments
var bmp = new Bitmap(clientRectangle.Width, clientRectangle.Height, PixelFormat.Format24bppRgb);
using (Graphics g = Graphics.FromImage(bmp))
{
g.CopyFromScreen(new Point(left, top), Point.Empty, rectangle.Size);
}
// Place the above image in the same canvas size as before
Bitmap backingImage = new Bitmap(rectangle.Width, rectangle.Height);
using (Graphics gfx = Graphics.FromImage(backingImage))
{
using (SolidBrush brush = new SolidBrush(Color.FromArgb(255, 0, 0)))
{
gfx.FillRectangle(brush, 0, 0, rectangle.Width, rectangle.Height);
gfx.DrawImage(bmp, new Point(0, top));
}
}
return backingImage;
}
}
}

View file

@ -1,66 +0,0 @@
using Gma.System.MouseKeyHook;
using System;
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<Point> OnPanelSelectionAdded;
public static event EventHandler OnPanelSelectionRemoved;
public static void StartHook()
{
if (_mouseHook == null)
{
_mouseHook = Hook.GlobalEvents();
_mouseHook.MouseDownExt += HandleMouseHookMouseDownExt;
}
}
public static void EndHook()
{
if (_mouseHook != null)
{
_mouseHook.MouseDownExt -= HandleMouseHookMouseDownExt;
_mouseHook.Dispose();
_mouseHook = null;
}
}
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));
}
}
}
}
}

View file

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

View file

@ -1,100 +0,0 @@
using MSFSPopoutPanelManager.Model;
using MSFSPopoutPanelManager.Shared;
using System;
using System.Collections.Generic;
namespace MSFSPopoutPanelManager.Provider
{
public class PanelSelectionManager
{
private UserProfileManager _userProfileManager;
private int _panelIndex;
private List<PanelSourceCoordinate> _panelCoordinates;
public event EventHandler OnPanelSelectionCompleted;
public event EventHandler<EventArgs<PanelSourceCoordinate>> OnPanelLocationAdded;
public event EventHandler OnPanelLocationRemoved;
public event EventHandler OnAllPanelLocationsRemoved;
public UserProfile UserProfile { get; set; }
public AppSetting AppSetting { get; set; }
public PanelSelectionManager(UserProfileManager userProfileManager)
{
_userProfileManager = userProfileManager;
InputHookManager.OnPanelSelectionAdded += HandleOnPanelSelectionAdded;
InputHookManager.OnPanelSelectionRemoved += HandleOnPanelSelectionRemoved;
InputHookManager.OnPanelSelectionCompleted += HandleOnPanelSelectionCompleted;
}
public void Start()
{
_panelIndex = 1;
_panelCoordinates = new List<PanelSourceCoordinate>();
//ShowPanelLocationOverlay(_panelCoordinates, true);
InputHookManager.SubscribeToPanelSelectionEvent = true;
InputHookManager.StartHook();
}
public void ShowPanelLocationOverlay(List<PanelSourceCoordinate> panelCoordinates, bool show)
{
// close all overlays
OnAllPanelLocationsRemoved?.Invoke(this, null);
if (show && panelCoordinates.Count > 0)
{
foreach (var coor in panelCoordinates)
{
var panelSourceCoordinate = new PanelSourceCoordinate() { PanelIndex = coor.PanelIndex, X = coor.X, Y = coor.Y };
OnPanelLocationAdded?.Invoke(this, new EventArgs<PanelSourceCoordinate>(panelSourceCoordinate));
}
}
}
private void HandleOnPanelSelectionAdded(object sender, System.Drawing.Point e)
{
var newPanelCoordinates = new PanelSourceCoordinate() { PanelIndex = _panelIndex, X = e.X, Y = e.Y };
_panelCoordinates.Add(newPanelCoordinates);
_panelIndex++;
OnPanelLocationAdded?.Invoke(this, new EventArgs<PanelSourceCoordinate>(newPanelCoordinates));
}
private void HandleOnPanelSelectionRemoved(object sender, EventArgs e)
{
if (_panelCoordinates.Count > 0)
{
_panelCoordinates.RemoveAt(_panelCoordinates.Count - 1);
_panelIndex--;
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)
{
InputEmulationManager.SaveCustomView(simualatorProcess.Handle, AppSetting.AutoPanningKeyBinding);
}
}
// Assign and save panel coordinates to active profile
UserProfile.PanelSourceCoordinates.Clear();
_panelCoordinates.ForEach(c => UserProfile.PanelSourceCoordinates.Add(c));
UserProfile.PanelConfigs.Clear();
UserProfile.IsLocked = false;
_userProfileManager.WriteUserProfiles();
InputHookManager.SubscribeToPanelSelectionEvent = false;
OnPanelSelectionCompleted?.Invoke(this, null);
}
}
}

View file

@ -1,43 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<RootNamespace>MSFSPopoutPanelManager.Provider</RootNamespace>
<PackageId>MSFS 2020 Popout Panel Manager Provider</PackageId>
<Product>MSFS 2020 Popout Panel Manager Provider</Product>
<Version>3.3.7.0</Version>
<Authors>Stanley Kwok</Authors>
<Copyright>Stanley Kwok 2021</Copyright>
<PackageProjectUrl>https://github.com/hawkeye-stan/msfs-popout-panel-manager</PackageProjectUrl>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon />
<StartupObject />
<Platforms>x64</Platforms>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MouseKeyHook" Version="5.6.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="WindowsInput" Version="6.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FsConnector\FsConnector.csproj" />
<ProjectReference Include="..\Model\Model.csproj" />
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>
</Project>

View file

@ -1,189 +0,0 @@
using MSFSPopoutPanelManager.FsConnector;
using MSFSPopoutPanelManager.Shared;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
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 bool _isPowerOnForPopOut;
private bool _isTrackIRManaged;
public event EventHandler OnConnected;
public event EventHandler OnDisconnected;
public event EventHandler<EventArgs<dynamic>> 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)
{
// Wait for _simData.AtcOnParkingSpot to refresh
Thread.Sleep(MSFS_DATA_REFRESH_TIMEOUT + 250);
if (isRequiredForColdStart && _simData != null && (_simData.AtcOnParkingSpot || !_simData.ElectricalMasterBattery))
{
_isPowerOnForPopOut = true;
_simConnector.TransmitActionEvent(ActionEvent.KEY_MASTER_BATTERY_SET, 1);
Thread.Sleep(100);
_simConnector.TransmitActionEvent(ActionEvent.KEY_TOGGLE_AVIONICS_MASTER, 1);
Thread.Sleep(100);
}
}
public void TurnOffpower()
{
if (_isPowerOnForPopOut)
{
_simConnector.TransmitActionEvent(ActionEvent.KEY_TOGGLE_AVIONICS_MASTER, 1);
Thread.Sleep(100);
_simConnector.TransmitActionEvent(ActionEvent.KEY_MASTER_BATTERY_SET, 0);
Thread.Sleep(100);
_isPowerOnForPopOut = false;
}
}
public void TurnOffTrackIR()
{
Debug.Write("TrackIR OFF............" + Environment.NewLine);
if (_simData != null && _simData.TrackIREnable)
{
SetTrackIREnable(false);
_isTrackIRManaged = true;
}
}
public void TurnOnTrackIR()
{
Debug.Write("TrackIR ON............" + Environment.NewLine);
if (_isTrackIRManaged && _simData != null && !_simData.TrackIREnable)
{
SetTrackIREnable(true);
_isTrackIRManaged = false;
}
}
private void SetTrackIREnable(bool enable)
{
// Wait for _simData.ElectricalMasterBattery to refresh
Thread.Sleep(MSFS_DATA_REFRESH_TIMEOUT + 250);
// It is prop3 in SimConnectStruct (by DataDefinitions.cs)
SimConnectStruct simConnectStruct = new SimConnectStruct();
simConnectStruct.Prop01 = _simData.Title; // must set "Title" for TrackIR variable to write correctly
simConnectStruct.Prop02 = _simData.ElectricalMasterBattery ? Convert.ToDouble(1) : Convert.ToDouble(0); // must set "ElectricalMasterBattery" for TrackIR variable to write correctly
simConnectStruct.Prop03 = enable ? Convert.ToDouble(1) : Convert.ToDouble(0); // this is the TrackIR variable
simConnectStruct.Prop04 = _simData.AtcOnParkingSpot ? Convert.ToDouble(1) : Convert.ToDouble(0); // must set "AtcOnParkingSpot" for TrackIR variable to write correctly
_simConnector.SetDataObject(simConnectStruct);
}
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<dynamic> e)
{
_simData = e.Value;
OnSimConnectDataRefreshed?.Invoke(this, new EventArgs<dynamic>(e.Value));
}
private List<SimConnectSystemEvent> _systemEventBuffer;
private List<SimConnectSystemEvent> FlightRestartBeginBufferDef = new List<SimConnectSystemEvent>() { SimConnectSystemEvent.SIMSTOP, SimConnectSystemEvent.VIEW };
private List<SimConnectSystemEvent> FlightRestartEndBufferDef = new List<SimConnectSystemEvent>() { SimConnectSystemEvent.SIMSTART, SimConnectSystemEvent.VIEW };
private List<SimConnectSystemEvent> FlightStartBufferDef = new List<SimConnectSystemEvent>() { SimConnectSystemEvent.SIMSTOP, SimConnectSystemEvent.SIMSTART, SimConnectSystemEvent.SIMSTOP, SimConnectSystemEvent.SIMSTART, SimConnectSystemEvent.VIEW };
private List<SimConnectSystemEvent> FlightEndBufferDef = new List<SimConnectSystemEvent>() { SimConnectSystemEvent.SIMSTOP, SimConnectSystemEvent.SIMSTART, SimConnectSystemEvent.VIEW, SimConnectSystemEvent.SIMSTOP, SimConnectSystemEvent.SIMSTART };
private bool _systemEventInFlightRestartSequence;
private void HandleReceiveSystemEvent(object sender, EventArgs<SimConnectSystemEvent> e)
{
if (_systemEventBuffer == null)
_systemEventBuffer = new List<SimConnectSystemEvent>();
var systemEvent = e.Value;
_systemEventBuffer.Add(systemEvent);
Debug.WriteLine($"SimConnectSystemEvent Received: {systemEvent}");
if (_systemEventBuffer.TakeLast(2).SequenceEqual(FlightRestartBeginBufferDef))
{
OnFlightStopped?.Invoke(this, null);
_systemEventBuffer = null;
_systemEventInFlightRestartSequence = true;
}
else if (_systemEventInFlightRestartSequence && _systemEventBuffer.TakeLast(2).SequenceEqual(FlightRestartEndBufferDef))
{
OnFlightStarted?.Invoke(this, null);
_systemEventBuffer = null;
_systemEventInFlightRestartSequence = false;
}
else if (_systemEventBuffer.TakeLast(5).SequenceEqual(FlightStartBufferDef))
{
OnFlightStarted?.Invoke(this, null);
_systemEventBuffer = null;
}
else if (_systemEventBuffer.TakeLast(5).SequenceEqual(FlightEndBufferDef))
{
OnFlightStopped?.Invoke(this, null);
_systemEventBuffer = null;
}
}
}
}

View file

@ -1,136 +0,0 @@
using MSFSPopoutPanelManager.Model;
using MSFSPopoutPanelManager.Shared;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
namespace MSFSPopoutPanelManager.Provider
{
public class UserProfileManager
{
private const string USER_PROFILE_DATA_FILENAME = "userprofiledata.json";
public ObservableCollection<UserProfile> 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<UserProfile>(); // Using Shared/ObjectExtensions.cs extension method
copiedProfile.BindingAircraftLiveries = new ObservableCollection<string>();
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 void AddProfileBinding(string planeTitle, int activeProfileId)
{
var boundProfile = UserProfiles.FirstOrDefault(p => p.BindingAircraftLiveries.ToList().Exists(p => p == planeTitle));
if (boundProfile != null)
{
Logger.LogStatus($"Unable to add binding to the profile because '{planeTitle}' was already bound to profile '{boundProfile.ProfileName}'.", StatusMessageType.Error);
return;
}
UserProfiles.First(p => p.ProfileId == activeProfileId).BindingAircraftLiveries.Add(planeTitle);
WriteUserProfiles();
Logger.LogStatus($"Binding for the profile has been added successfully.", StatusMessageType.Info);
}
public void DeleteProfileBinding(string planeTitle, int activeProfileId)
{
UserProfiles.First(p => p.ProfileId == activeProfileId).BindingAircraftLiveries.Remove(planeTitle);
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<UserProfile>(JsonConvert.DeserializeObject<List<UserProfile>>(reader.ReadToEnd()));
}
}
catch
{
UserProfiles = new ObservableCollection<UserProfile>(new List<UserProfile>());
}
}
public void WriteUserProfiles()
{
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;
}
}
}

View file

@ -1,154 +0,0 @@
using MSFSPopoutPanelManager.Model;
using System;
using System.Drawing;
using System.Runtime.InteropServices;
namespace MSFSPopoutPanelManager.Provider
{
public class WindowManager
{
public static void ApplyHidePanelTitleBar(IntPtr handle, bool hideTitleBar)
{
var currentStyle = PInvoke.GetWindowLong(handle, PInvokeConstant.GWL_STYLE).ToInt64();
if (hideTitleBar)
PInvoke.SetWindowLong(handle, PInvokeConstant.GWL_STYLE, (uint)(currentStyle & ~(PInvokeConstant.WS_CAPTION | PInvokeConstant.WS_SIZEBOX)));
else
PInvoke.SetWindowLong(handle, PInvokeConstant.GWL_STYLE, (uint)(currentStyle | (PInvokeConstant.WS_CAPTION | PInvokeConstant.WS_SIZEBOX)));
}
public static void ApplyAlwaysOnTop(IntPtr handle, 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 (alwaysOnTop)
PInvoke.SetWindowPos(handle, new IntPtr(PInvokeConstant.HWND_TOPMOST), panelRectangle.Left, panelRectangle.Top, newWidth, newHeight, PInvokeConstant.SWP_ALWAYS_ON_TOP);
else
PInvoke.SetWindowPos(handle, new IntPtr(PInvokeConstant.HWND_NOTOPMOST), panelRectangle.Left, panelRectangle.Top, newWidth, newHeight, 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, PanelType.WPFWindow, alwaysOnTop, new Rectangle(rect.X, rect.Y, clientRectangle.Width, clientRectangle.Height));
}
public static void CloseWindow(IntPtr handle)
{
PInvoke.SendMessage(handle, PInvokeConstant.WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
}
public static void MoveWindow(IntPtr handle, int x, int y)
{
Rectangle rectangle;
PInvoke.GetClientRect(handle, out rectangle);
PInvoke.MoveWindow(handle, x, y, rectangle.Width, rectangle.Height, true);
}
public static void MoveWindow(IntPtr handle, PanelType panelType, 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(handle, x, y, newWidth, newHeight, true);
}
public static void MoveWindowWithMsfsBugOverrirde(IntPtr handle, PanelType panelType, 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(handle, x, y, newWidth, newHeight, 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
System.Threading.Thread.Sleep(200);
Rectangle rectangle;
PInvoke.GetWindowRect(handle, out rectangle);
if (rectangle.Left != originalX)
PInvoke.MoveWindow(handle, originalX, y, newWidth, newHeight, false);
}
public static void MinimizeWindow(IntPtr handle)
{
PInvoke.ShowWindow(handle, PInvokeConstant.SW_MINIMIZE);
}
public static void BringWindowToForeground(IntPtr handle)
{
PInvoke.ShowWindowAsync(new HandleRef(null, handle), PInvokeConstant.SW_RESTORE);
PInvoke.SetForegroundWindow(handle);
}
public static void CloseAllCustomPopoutPanels()
{
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);
}
}
public static IntPtr FindWindowByCaption(string caption)
{
return PInvoke.FindWindowByCaption(IntPtr.Zero, caption);
}
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);
var caption = PInvoke.GetWindowText(hwnd);
if (className == "AceApp" && caption.IndexOf("WINDOW") > -1) // For multi monitor window, do nothing
return true;
if (className == "AceApp" && (caption.IndexOf("(Custom)") > -1 || caption == String.Empty)) // Only close non-builtin pop out panels
{
WindowManager.CloseWindow(hwnd);
}
else if (className == "AceApp" && caption.IndexOf("Microsoft Flight Simulator") == -1) // For builtin pop out (ATC, VFR Map, ect)
{
WindowManager.MoveWindow(hwnd, 0, 0);
}
return true;
}
}
}

View file

@ -1,13 +0,0 @@
using System;
namespace MSFSPopoutPanelManager.Provider
{
public class WindowProcess
{
public int ProcessId { get; set; }
public string ProcessName { get; set; }
public IntPtr Handle { get; set; }
}
}

View file

@ -1,7 +1,7 @@
# MSFS Pop Out Panel Manager # MSFS Pop Out Panel Manager
<p align="center"> <p align="center">
<img src="images/doc/panel_selection.png" width="600" hspace="10"/> <img src="assets/readme/images/panel_selection.png" width="600" hspace="10"/>
</p> </p>
MSFS Pop Out Panel Manager is an application for MSFS 2020 which helps pop out, save and position pop out panels to be used by utilities such as Sim Innovations Air Manager or to place pop out panels onto your screen or another monitor at predetermined locations automatically. MSFS Pop Out Panel Manager is an application for MSFS 2020 which helps pop out, save and position pop out panels to be used by utilities such as Sim Innovations Air Manager or to place pop out panels onto your screen or another monitor at predetermined locations automatically.
@ -45,7 +45,7 @@ What if you can do the setup once by defining on screen where the pop out panels
2. Start the application **MSFSPopoutPanelManager.exe** and it will automatically connect when MSFS/SimConnect starts. You maybe prompt to download .NET framework 5.0 x64 desktop runtime. Please see the screenshot below to download and install x64 desktop version of the framework. 2. Start the application **MSFSPopoutPanelManager.exe** and it will automatically connect when MSFS/SimConnect starts. You maybe prompt to download .NET framework 5.0 x64 desktop runtime. Please see the screenshot below to download and install x64 desktop version of the framework.
<p align="center"> <p align="center">
<img src="images/doc/framework_download.png" width="1000" hspace="10"/> <img src="assets/readme/images/framework_download.png" width="1000" hspace="10"/>
</p> </p>
## How to Update ## How to Update
@ -67,19 +67,19 @@ What if you can do the setup once by defining on screen where the pop out panels
1. First start the application and the game. Once you're in the main menu of the game, you can create a new profile by clicking the "plus" button in step 1 of the app. 1. First start the application and the game. Once you're in the main menu of the game, you can create a new profile by clicking the "plus" button in step 1 of the app.
<p align="center"> <p align="center">
<img src="images/doc/add_profile.png" width="900" hspace="10"/> <img src="assets/readme/images/add_profile.png" width="900" hspace="10"/>
</p> </p>
2. For step 2, if you want to associate the profile to the current aircraft livery 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 livery name. The aircraft livery title will become green once the livery is bound to the profile. Your chosen livery may not be available to select in the application for your newly selected plane until a flight is started. 2. For step 2, if you want to associate the profile to the current aircraft livery 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 livery name. The aircraft livery title will become green once the livery is bound to the profile. Your chosen livery may not be available to select in the application for your newly selected plane until a flight is started.
<p align="center"> <p align="center">
<img src="images/doc/bind_profile_to_livery.png" width="900" hspace="10"/> <img src="assets/readme/images/bind_profile_to_livery.png" width="900" hspace="10"/>
</p> </p>
3. Now start a flight with your chosen aircraft. Once your flight is started, 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 selection is completed. You can also move the number circles at this point to do final adjustment. 3. Now start a flight with your chosen aircraft. Once your flight is started, 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 selection is completed. You can also move the number circles at this point to do final adjustment.
<p align="center"> <p align="center">
<img src="images/doc/after_panel_selection.png" width="900" hspace="10"/> <img src="assets/readme/images/after_panel_selection.png" width="900" hspace="10"/>
</p> </p>
4. Next, 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. 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 as needed. 4. Next, 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. 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 as needed.
@ -87,7 +87,7 @@ What if you can do the setup once by defining on screen where the pop out panels
5. You can now start panel configuration by dragging the pop out panels into their final position (to your main monitor or another monitor). 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 data grid cell first (X-Pos, Y-Pos, Width, Height). You can also check "Always on Top", "Hide Title Bar", or "Full Screen Mode" if desire. If the panel is touch capable, you can check "Touch Enabled". Please see [Touch Enable Pop Out Feature](#touch-enable-pop-out-feature) regarding this experimental feature. Once all the panels are at their final position, just click "Lock Panel" to prevent further panel changes. 5. You can now start panel configuration by dragging the pop out panels into their final position (to your main monitor or another monitor). 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 data grid cell first (X-Pos, Y-Pos, Width, Height). You can also check "Always on Top", "Hide Title Bar", or "Full Screen Mode" if desire. If the panel is touch capable, you can check "Touch Enabled". Please see [Touch Enable Pop Out Feature](#touch-enable-pop-out-feature) regarding this experimental feature. Once all the panels are at their final position, just click "Lock Panel" to prevent further panel changes.
<p align="center"> <p align="center">
<img src="images/doc/panel_configuration.png" width="900" hspace="10"/> <img src="assets/readme/images/panel_configuration.png" width="900" hspace="10"/>
</p> </p>
6. To test if everything is working, please click "Restart" in the File menu. This will close all pop outs. Now click "Start Pop Out" and see the magic happens! 6. To test if everything is working, please click "Restart" in the File menu. This will close all pop outs. Now click "Start Pop Out" and see the magic happens!
@ -95,11 +95,11 @@ What if you can do the setup once by defining on screen where the pop out panels
7. With auto panning feature enabled in preferences setting, 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. 7. With auto panning feature enabled in preferences setting, 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.
<p align="center"> <p align="center">
<img src="images/doc/autopanning_1.png" width="900" hspace="10"/> <img src="assets/readme/images/autopanning_1.png" width="900" hspace="10"/>
</p> </p>
<p align="center"> <p align="center">
<img src="images/doc/autopanning_2.png" width="900" hspace="10"/> <img src="assets/readme/images/autopanning_2.png" width="900" hspace="10"/>
</p> </p>
<hr> <hr>
@ -148,13 +148,13 @@ In MSFS, when operating the above panels with pop outs, there are currently 2 li
- 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 havent 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 theyre 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. - 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 havent 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 theyre 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.
<p align="center"> <p align="center">
<img src="images/doc/touch_support_bug.png" width="900" hspace="10"/> <img src="assets/readme/images/touch_support_bug.png" width="900" hspace="10"/>
</p> </p>
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. 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.
<p align="center"> <p align="center">
<img src="images/doc/touch_support_bug_2.png" width="900" hspace="10"/> <img src="assets/readme/images/touch_support_bug_2.png" width="900" hspace="10"/>
</p> </p>
#### How to enable touch support #### How to enable touch support

2
ReactClient/.env Normal file
View file

@ -0,0 +1,2 @@
BROWSER=none
PORT=30101

23
ReactClient/.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

34128
ReactClient/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

51
ReactClient/package.json Normal file
View file

@ -0,0 +1,51 @@
{
"name": "pwa",
"version": "0.1.0",
"private": true,
"dependencies": {
"@elfalem/leaflet-curve": "^0.8.2",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@mui/icons-material": "^5.0.1",
"@mui/material": "^5.0.1",
"@mui/styles": "^5.0.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"console-feed": "^3.2.2",
"geolib": "^3.3.1",
"json-ref-lite": "^1.1.0",
"leaflet": "^1.7.1",
"leaflet-easybutton": "^2.4.0",
"leaflet-marker-rotation": "^0.4.0",
"leaflet.marker.slideto": "^0.2.0",
"react": "^17.0.2",
"react-dial-knob": "^1.3.0",
"react-dom": "^17.0.2",
"react-leaflet": "^3.2.1",
"react-leaflet-bing-v2": "^5.2.3",
"react-lineto": "^3.2.1",
"react-router-dom": "^5.3.0",
"react-scripts": "4.0.3",
"rimraf": "^2.7.1"
},
"scripts": {
"start": "rimraf ./build && react-scripts start",
"build": "rimraf ./build && react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"serve": "http-server build -p 27010",
"buildserve": "rimraf ./build && react-scripts build && http-server build -p 27010"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": [
">0.2%",
"not dead",
"not op_mini all"
]
}

View file

@ -0,0 +1,342 @@
[
{
"planeId": "kodiak",
"name": "Kodiak 100",
"panels": [
{
"panelId": "kodiak",
"name": "Kodiak Full Instrumentation",
"rootPath": "kodiak",
"panelSize": {
"width": 2458,
"height": 1310
},
"showMenuBar": true,
"enableMap": true,
"subPanels": [
{
"panelId": "pfd-mfd",
"name": "PFD MFD",
"definitions": "KODIAK_PFD_MFD_DEF",
"rootPath": "pfdmfd",
"left": 0,
"top": 0,
"scale": 1
},
{
"panelId": "electrical",
"name": "Electrical",
"definitions": "KODIAK_ELECTRICAL_DEF",
"rootPath": "electrical",
"left": 0,
"top": 915,
"scale": 1
},
{
"panelId": "lights",
"name": "Lights",
"definitions": "KODIAK_LIGHTS_DEF",
"rootPath": "lights",
"left": 830,
"top": 915,
"scale": 1
},
{
"panelId": "fuel",
"name": "Fuel",
"definitions": "KODIAK_FUEL_DEF",
"rootPath": "fuel",
"left": 1770,
"top": 980,
"scale": 1
},
{
"panelId": "oxygen",
"name": "Oxygen",
"definitions": "KODIAK_OXYGEN_DEF",
"rootPath": "oxygen",
"left": 2230,
"top": 985,
"scale": 1
}
]
},
{
"panelId": "kodiak-pfd",
"name": "Kodiak PFD",
"rootPath": "kodiak",
"panelSize": {
"width": 1408,
"height": 914
},
"showMenuBar": false,
"enableMap": false,
"subPanels": [
{
"panelId": "pfd",
"name": "PFD",
"definitions": "KODIAK_PFD_ONLY_DEF",
"rootPath": "pfdmfd",
"left": 0,
"top": 0,
"scale": 1
}
]
},
{
"panelId": "kodiak-mfd",
"name": "Kodiak MFD",
"rootPath": "kodiak",
"panelSize": {
"width": 1408,
"height": 914
},
"showMenuBar": false,
"enableMap": false,
"subPanels": [
{
"panelId": "mfd",
"name": "MFD",
"definitions": "KODIAK_MFD_ONLY_DEF",
"rootPath": "pfdmfd",
"left": 0,
"top": 0,
"scale": 1
}
]
},
{
"panelId": "kodiak-electrical",
"name": "Kodiak Electrical",
"rootPath": "kodiak",
"panelSize": {
"width": 812,
"height": 377
},
"showMenuBar": false,
"enableMap": false,
"subPanels": [
{
"panelId": "electrical",
"name": "Electrical",
"definitions": "KODIAK_ELECTRICAL_DEF",
"rootPath": "electrical",
"left": 0,
"top": 0,
"scale": 1
}
]
},
{
"panelId": "kodiak-lights",
"name": "Kodiak Lights",
"rootPath": "kodiak",
"panelSize": {
"width": 929,
"height": 380
},
"showMenuBar": false,
"enableMap": false,
"subPanels": [
{
"panelId": "lights",
"name": "Lights",
"definitions": "KODIAK_LIGHTS_DEF",
"rootPath": "lights",
"left": 0,
"top": 0,
"scale": 1
}
]
},
{
"panelId": "kodiak-fuel",
"name": "Kodiak Fuel Switch",
"rootPath": "kodiak",
"panelSize": {
"width": 440,
"height": 226
},
"showMenuBar": false,
"enableMap": false,
"subPanels": [
{
"panelId": "fuel",
"name": "Fuel",
"definitions": "KODIAK_FUEL_DEF",
"rootPath": "fuel",
"left": 0,
"top": 0,
"scale": 1
}
]
},
{
"panelId": "kodiak-oxygen",
"name": "Kodiak Oxygen Switch",
"rootPath": "kodiak",
"panelSize": {
"width": 215,
"height": 215
},
"showMenuBar": false,
"enableMap": false,
"subPanels": [
{
"panelId": "oxygen",
"name": "Oxygen",
"definitions": "KODIAK_OXYGEN_DEF",
"rootPath": "oxygen",
"left": 0,
"top": 0,
"scale": 1
}
]
}
]
},
{
"planeId": "pmdg-737-700",
"name": "PMDG 737-700",
"panels": [
{
"panelId": "pmdg-737-700-full-instrumentation",
"name": "PMDG 737 Full Instrumentation",
"rootPath": "pmdg-737-700",
"panelSize": {
"width": 1920,
"height": 1010
},
"showMenuBar": true,
"enableMap": true,
"subPanels": [
{
"panelId": "mcp",
"name": "MCP",
"rootPath": "mcp",
"definitions": "PMDG_737_700_MCP_DEF",
"left": 0,
"top": 0,
"scale": 1
},
{
"panelId": "efis-cpt",
"name": "EFIS-CPT",
"rootPath": "efis-cpt",
"definitions": "PMDG_737_700_EFIS_CPT_DEF",
"left": 90,
"top": 335,
"scale": 1
},
{
"panelId": "xpndr",
"name": "XPNDR",
"rootPath": "xpndr",
"definitions": "PMDG_737_700_XPNDR_DEF",
"left": 210,
"top": 735,
"scale": 1
},
{
"panelId": "radio",
"name": "Radio",
"rootPath": "radio",
"definitions": "PMDG_737_700_RADIO_DEF",
"left": 850,
"top": 335,
"scale": 1
}
]
},
{
"panelId": "pmdg-737-700-mcp",
"name": "PMDG 737 MCP",
"rootPath": "pmdg-737-700",
"panelSize": {
"width": 1920,
"height": 329
},
"showMenuBar": true,
"enableMap": false,
"subPanels": [
{
"panelId": "mcp",
"name": "MCP",
"rootPath": "mcp",
"definitions": "PMDG_737_700_MCP_DEF",
"left": 0,
"top": 0,
"scale": 1
}
]
},
{
"panelId": "pmdg-737-700-radio",
"name": "PMDG 737 Radio",
"rootPath": "pmdg-737-700",
"panelSize": {
"width": 995,
"height": 485
},
"showMenuBar": false,
"enableMap": false,
"subPanels": [
{
"panelId": "radio",
"name": "Radio",
"rootPath": "radio",
"definitions": "PMDG_737_700_RADIO_DEF",
"left": 0,
"top": 0,
"scale": 1
}
]
},
{
"panelId": "pmdg-737-700-xpndr",
"name": "PMDG 737 Transponder",
"rootPath": "pmdg-737-700",
"panelSize": {
"width": 498,
"height": 259
},
"showMenuBar": false,
"enableMap": false,
"subPanels": [
{
"panelId": "xpndr",
"name": "XPNDR",
"rootPath": "xpndr",
"definitions": "PMDG_737_700_XPNDR_DEF",
"left": 0,
"top": 0,
"scale": 1
}
]
},
{
"panelId": "pmdg-737-700-efis-cpt",
"name": "PMDG 737 EFIS (Captain)",
"rootPath": "pmdg-737-700",
"panelSize": {
"width": 724,
"height": 394
},
"showMenuBar": false,
"enableMap": false,
"subPanels": [
{
"panelId": "efis-cpt",
"name": "EFIS-CPT",
"rootPath": "efis-cpt",
"definitions": "PMDG_737_700_EFIS_CPT_DEF",
"left": 0,
"top": 0,
"scale": 1
}
]
}
]
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,380 @@
[
{
"propName": "SIM_RATE",
"variableName": "SIMULATION RATE",
"simConnectUnit": "number",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": "toFixed2"
},
{
"propName": "GPS_LAT",
"variableName": "GPS POSITION LAT",
"simConnectUnit": "degrees",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "GPS_LON",
"variableName": "GPS POSITION LON",
"simConnectUnit": "degrees",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "PLANE_HEADING_TRUE",
"variableName": "PLANE HEADING DEGREES TRUE",
"simConnectUnit": "degrees",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": "toFixed0"
},
{
"propName": "PLANE_AIRSPEED",
"variableName": "AIRSPEED INDICATED",
"simConnectUnit": "knots",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": "toFixed0"
},
{
"propName": "FLAPS",
"variableName": "TRAILING EDGE FLAPS LEFT ANGLE",
"simConnectUnit": "degrees",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": "toFixed0"
},
{
"propName": "ELEVATOR_TRIM",
"variableName": "ELEVATOR TRIM PCT",
"simConnectUnit": "percent",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": "toFixed1"
},
{
"propName": "RUDDER_TRIM",
"variableName": "RUDDER TRIM",
"simConnectUnit": "radians",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": "toFixed2"
},
{
"propName": "AILERON_TRIM",
"variableName": "AILERON TRIM",
"simConnectUnit": "radians",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": "toFixed3"
},
{
"propName": "BRAKE_PARKING_INDICATOR",
"variableName": "BRAKE PARKING INDICATOR",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": "toFixed0"
},
{
"propName": "GEAR_POSITION",
"variableName": "GEAR POSITION",
"simConnectUnit": "enum",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "GEAR_CENTER_POSITION",
"variableName": "GEAR CENTER POSITION",
"simConnectUnit": "percent",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "GEAR_LEFT_POSITION",
"variableName": "GEAR LEFT POSITION",
"simConnectUnit": "percent",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "GEAR_RIGHT_POSITION",
"variableName": "GEAR RIGHT POSITION",
"simConnectUnit": "percent",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "AP_YAW_DAMPER_ON",
"variableName": "AUTOPILOT YAW DAMPER",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "BATTERY_MASTER_ON",
"variableName": "ELECTRICAL MASTER BATTERY",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "AVIONICS_MASTER_ON",
"variableName": "AVIONICS MASTER SWITCH",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "AP_MASTER_ON",
"variableName": "AUTOPILOT MASTER",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "LIGHT_TAXI_ON",
"variableName": "LIGHT TAXI",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "LIGHT_NAV_ON",
"variableName": "LIGHT NAV",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "LIGHT_BEACON_ON",
"variableName": "LIGHT BEACON",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "LIGHT_STROBE_ON",
"variableName": "LIGHT STROBE",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "LIGHT_WING_ON",
"variableName": "LIGHT WING",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "KODIAK_LANDING_LIGHT",
"variableName": "SWS_LIGHTING_Switch_Light_Landing",
"simConnectUnit": null,
"dataType": "Default",
"dataDefinitionType": "LVar",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "KODIAK_CABIN_LIGHT",
"variableName": "SWS_LIGHTING_Switch_Light_CABIN_12",
"simConnectUnit": null,
"dataType": "Default",
"dataDefinitionType": "LVar",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "KODIAK_PITOT_HEAT_1",
"variableName": "DEICE_Pitot_1",
"simConnectUnit": null,
"dataType": "Default",
"dataDefinitionType": "LVar",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "KODIAK_PITOT_HEAT_2",
"variableName": "DEICE_Pitot_2",
"simConnectUnit": null,
"dataType": "Default",
"dataDefinitionType": "LVar",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "KODIAK_AUX_BUS",
"variableName": "XMLVAR_AUX_Bus_ON",
"simConnectUnit": null,
"dataType": "Default",
"dataDefinitionType": "LVar",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "KODIAK_FUEL_PUMP",
"variableName": "SWS_FUEL_Switch_Pump_1",
"simConnectUnit": null,
"dataType": "Default",
"dataDefinitionType": "LVar",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "KODIAK_IGNITION",
"variableName": "SWS_ENGINE_Switch_Ignition_1",
"simConnectUnit": null,
"dataType": "Default",
"dataDefinitionType": "LVar",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "KODIAK_STARTER",
"variableName": "SWS_ENGINE_Switch_Starter_ThreeState_1",
"simConnectUnit": null,
"dataType": "Default",
"dataDefinitionType": "LVar",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "KODIAK_FUEL_SHUTOFF_LEFT",
"variableName": "SWS_Kodiak_TankSelector_1",
"simConnectUnit": null,
"dataType": "Default",
"dataDefinitionType": "LVar",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "KODIAK_FUEL_SHUTOFF_RIGHT",
"variableName": "SWS_Kodiak_TankSelector_2",
"simConnectUnit": null,
"dataType": "Default",
"dataDefinitionType": "LVar",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "KODIAK_OXYGEN",
"variableName": "XMLVAR_Oxygen",
"simConnectUnit": null,
"dataType": "Default",
"dataDefinitionType": "LVar",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "ENG_ANTI_ICE_1",
"variableName": "ENG ANTI ICE:1",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "PROP_DEICE_SWITCH",
"variableName": "PROP DEICE SWITCH:1",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "WINDSHIELD_DEICE_SWITCH",
"variableName": "WINDSHIELD DEICE SWITCH",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "ALTERNATOR_1_ON",
"variableName": "GENERAL ENG MASTER ALTERNATOR:1",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "ALTERNATOR_2_ON",
"variableName": "GENERAL ENG MASTER ALTERNATOR:2",
"simConnectUnit": "bool",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "KODIAK_GLARESHIELD_SETTING",
"variableName": "LIGHT POTENTIOMETER:3",
"simConnectUnit": "number",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "KODIAK_INSTRUMENTATION_LIGHT_SETTING",
"variableName": "LIGHT POTENTIOMETER:2",
"simConnectUnit": "number",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
},
{
"propName": "KODIAK_PANEL_LIGHT_SETTING",
"variableName": "LIGHT POTENTIOMETER:21",
"simConnectUnit": "number",
"dataType": "Float64",
"dataDefinitionType": "SimConnect",
"defaultValue": 0,
"JavaScriptFormatting": null
}
]

View file

@ -0,0 +1,389 @@
{
"id": "KODIAK_ELECTRICAL_DEF",
"value": {
"panelSize": {
"width": 812,
"height": 377
},
"backgroundImage": "background_electrical.png",
"controlSize": {
"buttonBase": {
"width": 57,
"height": 36
},
"toggleButtonBase": {
"width": 50,
"height": 98
},
"masterBase": {
"width": 80,
"height": 141
},
"rockerBase": {
"width": 55,
"height": 115
},
"buttonStatusIndicator": {
"width": 4,
"height": 32
}
},
"panelControlDefinitions": [
{
"id": "btn_yd",
"type": "imageButton",
"image": "button_yd.png",
"left": 46,
"top": 74,
"controlSize": {
"$ref": "#value.controlSize.buttonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "YAW_DAMPER_TOGGLE",
"actionType": "SimEventId"
}
]
}
},
{
"id": "btn_yd_indicator",
"type": "bindableImageButton",
"left": 105,
"top": 73,
"controlSize": {
"$ref": "#value.controlSize.buttonStatusIndicator"
},
"highlight": false,
"binding": {
"variable": "AP_YAW_DAMPER_ON",
"images": [
{
"url": "button_status_on.png",
"val": 0
},
{
"url": "button_status_on.png",
"val": 1
}
]
}
},
{
"id": "btn_lvl",
"type": "imageButton",
"image": "button_lvl.png",
"left": 46,
"top": 134,
"controlSize": {
"$ref": "#value.controlSize.buttonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "NO_ACTION",
"actionType": "SimEventId"
}
]
}
},
{
"id": "btn_bank",
"type": "imageButton",
"image": "button_bank.png",
"left": 46,
"top": 196,
"controlSize": {
"$ref": "#value.controlSize.buttonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "NO_ACTION",
"actionType": "SimEventId"
}
]
}
},
{
"id": "btn_spd",
"type": "imageButton",
"image": "button_spd.png",
"left": 46,
"top": 258,
"controlSize": {
"$ref": "#value.controlSize.buttonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "AP_SPD_VAR_SET",
"actionType": "SimEventId"
}
]
}
},
{
"id": "btn_master",
"type": "bindableImageButton",
"left": 155,
"top": 98,
"controlSize": {
"$ref": "#value.controlSize.masterBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "TOGGLE_MASTER_BATTERY",
"actionType": "SimEventId"
}
]
},
"binding": {
"variable": "BATTERY_MASTER_ON",
"images": [
{
"url": "master_off.png",
"val": 0
},
{
"url": "master_on.png",
"val": 1
}
]
}
},
{
"id": "btn_avn_bus",
"type": "bindableImageButton",
"left": 274,
"top": 106,
"controlSize": {
"$ref": "#value.controlSize.rockerBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "TOGGLE_AVIONICS_MASTER",
"actionType": "SimEventId"
}
]
},
"binding": {
"variable": "AVIONICS_MASTER_ON",
"images": [
{
"url": "rocker_off.png",
"val": 0
},
{
"url": "rocker_on.png",
"val": 1
}
]
}
},
{
"id": "btn_aux_bus",
"type": "bindableImageButton",
"left": 363,
"top": 106,
"controlSize": {
"$ref": "#value.controlSize.rockerBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "(L:XMLVAR_AUX_Bus_ON) ++ 2 % (>L:XMLVAR_AUX_Bus_ON)",
"actionType": "SimVarCode"
}
]
},
"binding": {
"variable": "KODIAK_AUX_BUS",
"images": [
{
"url": "rocker_off.png",
"val": 0
},
{
"url": "rocker_on.png",
"val": 1
}
]
}
},
{
"id": "btn_aux_fuel_pump",
"type": "bindableImageButton",
"left": 472,
"top": 47,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "(L:SWS_FUEL_Switch_Pump_1) ++ 3 % (>L:SWS_FUEL_Switch_Pump_1)",
"actionType": "SimVarCode"
}
]
},
"binding": {
"variable": "KODIAK_FUEL_PUMP",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_mid.png",
"val": 1
},
{
"url": "toggle_up.png",
"val": 2
}
]
}
},
{
"id": "btn_ignition",
"type": "bindableImageButton",
"left": 569,
"top": 47,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "(L:SWS_ENGINE_Switch_Ignition_1) ++ 2 % (>L:SWS_ENGINE_Switch_Ignition_1)",
"actionType": "SimVarCode"
}
]
},
"binding": {
"variable": "KODIAK_IGNITION",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_up.png",
"val": 1
}
]
}
},
{
"id": "btn_starter",
"type": "bindableImageButton",
"left": 666,
"top": 47,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "(L:SWS_ENGINE_Switch_Starter_ThreeState_1) ++ 3 % (>L:SWS_ENGINE_Switch_Starter_ThreeState_1)",
"actionType": "SimVarCode"
}
]
},
"binding": {
"variable": "KODIAK_STARTER",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_mid.png",
"val": 1
},
{
"url": "toggle_up.png",
"val": 2
}
]
}
},
{
"id": "btn_generator",
"type": "bindableImageButton",
"left": 569,
"top": 225,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "TOGGLE_ALTERNATOR1",
"actionType": "SimEventId"
}
]
},
"binding": {
"variable": "ALTERNATOR_1_ON",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_up.png",
"val": 1
}
]
}
},
{
"id": "btn_alternator",
"type": "bindableImageButton",
"left": 666,
"top": 225,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "TOGGLE_ALTERNATOR2",
"actionType": "SimEventId"
}
]
},
"binding": {
"variable": "ALTERNATOR_2_ON",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_up.png",
"val": 1
}
]
}
}
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,80 @@
{
"id": "KODIAK_FUEL_DEF",
"value": {
"panelSize": {
"width": 440,
"height": 226
},
"backgroundImage": "background_fuel.png",
"controlSize": {
"fuelLever": {
"width": 158,
"height": 158
}
},
"panelControlDefinitions": [
{
"id": "btn_fuel_left",
"type": "bindableImageButton",
"left": 50,
"top": 45,
"controlSize": {
"$ref": "#value.controlSize.fuelLever"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "(L:SWS_Kodiak_TankSelector_1) ++ 2 % (>L:SWS_Kodiak_TankSelector_1)",
"actionType": "SimVarCode"
}
]
},
"binding": {
"variable": "KODIAK_FUEL_SHUTOFF_LEFT",
"images": [
{
"url": "fuel_lever_left_off.png",
"val": 0
},
{
"url": "fuel_lever_left_on.png",
"val": 1
}
]
}
},
{
"id": "btn_fuel_right",
"type": "bindableImageButton",
"left": 232,
"top": 45,
"controlSize": {
"$ref": "#value.controlSize.fuelLever"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "(L:SWS_Kodiak_TankSelector_2) ++ 2 % (>L:SWS_Kodiak_TankSelector_2)",
"actionType": "SimVarCode"
}
]
},
"binding": {
"variable": "KODIAK_FUEL_SHUTOFF_RIGHT",
"images": [
{
"url": "fuel_lever_right_off.png",
"val": 0
},
{
"url": "fuel_lever_right_on.png",
"val": 1
}
]
}
}
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,488 @@
{
"id": "KODIAK_LIGHTS_DEF",
"value": {
"panelSize": {
"width": 929,
"height": 380
},
"backgroundImage": "background_lights.png",
"controlSize": {
"toggleButtonBase": {
"width": 50,
"height": 98
},
"lightKnob": {
"width": 70,
"height": 70
},
"iceKnob": {
"width": 65,
"height": 65
},
"rockerBase": {
"width": 55,
"height": 115
}
},
"panelControlDefinitions": [
{
"id": "btn_beacon_light",
"type": "bindableImageButton",
"left": 82,
"top": 49,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "TOGGLE_BEACON_LIGHTS",
"actionType": "SimEventId"
}
]
},
"binding": {
"variable": "LIGHT_BEACON_ON",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_up.png",
"val": 1
}
]
}
},
{
"id": "btn_strobe_light",
"type": "bindableImageButton",
"left": 178,
"top": 49,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "STROBES_TOGGLE",
"actionType": "SimEventId"
}
]
},
"binding": {
"variable": "LIGHT_STROBE_ON",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_up.png",
"val": 1
}
]
}
},
{
"id": "btn_nav_light",
"type": "bindableImageButton",
"left": 272,
"top": 49,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "TOGGLE_NAV_LIGHTS",
"actionType": "SimEventId"
}
]
},
"binding": {
"variable": "LIGHT_NAV_ON",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_up.png",
"val": 1
}
]
}
},
{
"id": "btn_taxi_light",
"type": "bindableImageButton",
"left": 367,
"top": 49,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "TOGGLE_TAXI_LIGHTS",
"actionType": "SimEventId"
}
]
},
"binding": {
"variable": "LIGHT_TAXI_ON",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_up.png",
"val": 1
}
]
}
},
{
"id": "btn_landing_light",
"type": "bindableImageButton",
"left": 485,
"top": 49,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "(L:SWS_LIGHTING_Switch_Light_Landing) ++ 3 % (>L:SWS_LIGHTING_Switch_Light_Landing)",
"actionType": "SimVarCode"
}
]
},
"binding": {
"variable": "KODIAK_LANDING_LIGHT",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_mid.png",
"val": 1
},
{
"url": "toggle_up.png",
"val": 2
}
]
}
},
{
"id": "btn_cabin_light",
"type": "bindableImageButton",
"left": 603,
"top": 49,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "(L:SWS_LIGHTING_Switch_Light_CABIN_12) ++ 3 % (>L:SWS_LIGHTING_Switch_Light_CABIN_12)",
"actionType": "SimVarCode"
}
]
},
"binding": {
"variable": "KODIAK_CABIN_LIGHT",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_mid.png",
"val": 1
},
{
"url": "toggle_up.png",
"val": 2
}
]
}
},
{
"id": "btn_instrument_light_knob",
"type": "imageButton",
"image": "knob_light_dual.png",
"left": 685,
"top": 69,
"controlSize": {
"$ref": "#value.controlSize.lightKnob"
},
"highlight": true,
"action": {
"touchActions": [
{
"action": "KODIAK_INSTRUMENTS_LIGHT_KNOB_SELECT",
"actionType": "SimEventId"
}
],
"useDualEncoder": true,
"encoderAction": {
"encoderLowerCW": "[/{KODIAK_GLARESHIELD_SETTING/} * 100 + 5] 100 min (>K:LIGHT_POTENTIOMETER_3_SET) 1 (>K:GLARESHIELD_LIGHTS_ON)",
"encoderLowerCCW": "[/{KODIAK_GLARESHIELD_SETTING/} * 100 - 5] 0 max (>K:LIGHT_POTENTIOMETER_3_SET)",
"encoderUpperCW": "[/{KODIAK_INSTRUMENTATION_LIGHT_SETTING/} * 100 + 5] 100 min (>K:LIGHT_POTENTIOMETER_2_SET) 1 (>K:PANEL_LIGHTS_ON)",
"encoderUpperCCW": "[/{KODIAK_INSTRUMENTATION_LIGHT_SETTING/} * 100 - 5] 0 max (>K:LIGHT_POTENTIOMETER_2_SET)",
"actionType": "SimVarCode"
}
}
},
{
"id": "btn_switch_panel_light_knob",
"type": "imageButton",
"image": "knob_light.png",
"left": 778,
"top": 69,
"controlSize": {
"$ref": "#value.controlSize.lightKnob"
},
"highlight": true,
"action": {
"touchActions": [
{
"action": "KODIAK_PANEL_LIGHT_KNOB_SELECT",
"actionType": "SimEventId"
}
],
"useEncoder": true,
"encoderAction": {
"encoderLowerCW": "[/{KODIAK_PANEL_LIGHT_SETTING/} * 100 + 5] 100 min (>K:LIGHT_POTENTIOMETER_21_SET) 1 (>K:PEDESTRAL_LIGHTS_ON)",
"encoderLowerCCW": "[/{KODIAK_PANEL_LIGHT_SETTING/} * 100 - 5] 0 max (>K:LIGHT_POTENTIOMETER_21_SET)",
"encoderUpperCW": "[/{KODIAK_PANEL_LIGHT_SETTING/} * 100 + 5] 100 min (>K:LIGHT_POTENTIOMETER_21_SET) 1 (>K:PEDESTRAL_LIGHTS_ON)",
"encoderUpperCCW": "[/{KODIAK_PANEL_LIGHT_SETTING/} * 100 - 5] 0 max (>K:LIGHT_POTENTIOMETER_21_SET)",
"actionType": "SimVarCode"
}
}
},
{
"id": "btn_engine_inlet_bypass_override",
"type": "image",
"image": "toggle_down.png",
"left": 82,
"top": 225,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false
},
{
"id": "btn_engine_inlet_bypass",
"type": "bindableImageButton",
"left": 178,
"top": 225,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "1 (>K:ANTI_ICE_TOGGLE_ENG1)",
"actionType": "SimVarCode"
}
]
},
"binding": {
"variable": "ENG_ANTI_ICE_1",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_up.png",
"val": 1
}
]
}
},
{
"id": "btn_pitot_heat_left",
"type": "bindableImageButton",
"left": 272,
"top": 225,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "1 (>K:PITOT_HEAT_TOGGLE)",
"actionType": "SimVarCode"
}
]
},
"binding": {
"variable": "KODIAK_PITOT_HEAT_1",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_up.png",
"val": 1
}
]
}
},
{
"id": "btn_pitot_heat_right",
"type": "bindableImageButton",
"left": 367,
"top": 225,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "2 (>K:PITOT_HEAT_TOGGLE)",
"actionType": "SimVarCode"
}
]
},
"binding": {
"variable": "KODIAK_PITOT_HEAT_2",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_up.png",
"val": 1
}
]
}
},
{
"id": "btn_ice_knob",
"type": "bindableImageButton",
"left": 480,
"top": 250,
"controlSize": {
"$ref": "#value.controlSize.iceKnob"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "TOGGLE_PROPELLER_DEICE",
"actionType": "SimEventId"
}
]
},
"binding": {
"variable": "PROP_DEICE_SWITCH",
"images": [
{
"url": "knob_ice.png",
"rotate": 0,
"val": 0
},
{
"url": "knob_ice.png",
"rotate": 95,
"val": 1
}
]
}
},
{
"id": "btn_wind_shield_heat",
"type": "bindableImageButton",
"left": 603,
"top": 225,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "KODIAK_100_DEICE_WINDSHIELD_TOGGLE",
"actionType": "SimEventId"
}
]
},
"binding": {
"variable": "WINDSHIELD_DEICE_SWITCH",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_up.png",
"val": 1
}
]
}
},
{
"id": "btn_backup_pump_heat",
"type": "image",
"image": "toggle_down.png",
"left": 696,
"top": 225,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false
},
{
"id": "btn_ice_light",
"type": "bindableImageButton",
"left": 790,
"top": 225,
"controlSize": {
"$ref": "#value.controlSize.toggleButtonBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "TOGGLE_WING_LIGHTS",
"actionType": "SimEventId"
}
]
},
"binding": {
"variable": "LIGHT_WING_ON",
"images": [
{
"url": "toggle_down.png",
"val": 0
},
{
"url": "toggle_up.png",
"val": 1
}
]
}
}
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,76 @@
{
"id": "KODIAK_OXYGEN_DEF",
"value": {
"panelSize": {
"width": 215,
"height": 215
},
"backgroundImage": "background_oxygen.png",
"controlSize": {
"oxygenBase": {
"width": 49,
"height": 69
},
"oxygenIndicatorBase": {
"width": 10,
"height": 107
}
},
"panelControlDefinitions": [
{
"id": "btn_oxygen",
"type": "bindableImageButton",
"left": 92,
"top": 82,
"controlSize": {
"$ref": "#value.controlSize.oxygenBase"
},
"highlight": false,
"action": {
"touchActions": [
{
"action": "(L:XMLVAR_Oxygen) ++ 2 % (>L:XMLVAR_Oxygen)",
"actionType": "SimVarCode"
}
]
},
"binding": {
"variable": "KODIAK_OXYGEN",
"images": [
{
"url": "oxygen_off.png",
"val": 0
},
{
"url": "oxygen_on.png",
"val": 1
}
]
}
},
{
"id": "btn_oxygen_indicator",
"type": "bindableImageButton",
"left": 66,
"top": 60,
"controlSize": {
"$ref": "#value.controlSize.oxygenIndicatorBase"
},
"highlight": false,
"binding": {
"variable": "KODIAK_OXYGEN",
"images": [
{
"url": "oxygen_indicator_off.png",
"val": 0
},
{
"url": "oxygen_indicator_on.png",
"val": 1
}
]
}
}
]
}
}

Some files were not shown because too many files have changed in this diff Show more