diff --git a/HandheldCompanion.iss b/HandheldCompanion.iss index 0743ce7b2..963a15025 100644 --- a/HandheldCompanion.iss +++ b/HandheldCompanion.iss @@ -1,17 +1,158 @@ -; ----------- -; CODE -; ----------- -[Code] +[Setup] +; ------------- +; SETUP +; ------------- +#ifndef Dependency_NoExampleSetup + +; requires netcorecheck.exe and netcorecheck_x64.exe (see download link below) +#define UseNetCoreCheck +#ifdef UseNetCoreCheck + #define UseDotNet80 +#endif + +;#define UseVC2005 +;#define UseVC2008 +;#define UseVC2010 +;#define UseVC2012 +;#define UseVC2013 +;#define UseVC2015To2019 + +#define UseDirectX +;Install ViGem first +#define UseViGem +#define UseHideHide +#define UseRTSS + +#define InstallerVersion '0.2' +#define MyAppSetupName 'Handheld Companion' +#define MyBuildId 'HandheldCompanion' +#define MyAppVersion '0.20.5.6' +#define MyAppPublisher 'BenjaminLSR' +#define MyAppCopyright 'Copyright @ BenjaminLSR' +#define MyAppURL 'https://github.com/Valkirie/HandheldCompanion' +#define MyAppExeName "HandheldCompanion.exe" +#define MyConfiguration "Release" + +#define RtssExe "RTSS.exe" +#define EncoderServer64Exe "EncoderServer64.exe" +#define RTSSHooksLoader64Exe "RTSSHooksLoader64.exe" +#define EncoderServerExe "EncoderServer.exe" +#define RTSSHooksLoaderExe "RTSSHooksLoader.exe" + +#define DotNetName ".NET Desktop Runtime" +#define DirectXName "DirectX Runtime" +#define ViGemName "ViGEmBus Setup" +#define HidHideName "HidHide Drivers" +#define RtssName "RTSS Setup" + +#define NewDotNetVersion "8.0.1" +#define NewDirectXVersion "9.29.1974" +#define NewViGemVersion "1.22.0.0" +#define NewHidHideVersion "1.5.212" +#define NewRtssVersion "7.3.6" + +//#define DotNetX64DownloadLink "https://download.visualstudio.microsoft.com/download/pr/b280d97f-25a9-4ab7-8a12-8291aa3af117/a37ed0e68f51fcd973e9f6cb4f40b1a7/windowsdesktop-runtime-8.0.0-win-x64.exe" +//#define DotNetX86DownloadLink "https://download.visualstudio.microsoft.com/download/pr/f9e3b581-059d-429f-9f0d-1d1167ff7e32/bd7661030cd5d66cd3eee0fd20b24540/windowsdesktop-runtime-8.0.0-win-x86.exe" + +#define DotNetX64DownloadLink "https://download.visualstudio.microsoft.com/download/pr/f18288f6-1732-415b-b577-7fb46510479a/a98239f751a7aed31bc4aa12f348a9bf/windowsdesktop-runtime-8.0.1-win-x64.exe" +#define DotNetX86DownloadLink "https://download.visualstudio.microsoft.com/download/pr/ca725693-6de7-4a4d-b8a4-4390b0387c66/ce13f2f016152d9b5f2d3c6537cc415b/windowsdesktop-runtime-8.0.1-win-x86.exe" + +#define DirectXDownloadLink "https://download.microsoft.com/download/1/7/1/1718CCC4-6315-4D8E-9543-8E28A4E18C4C/dxwebsetup.exe" +#define HidHideDownloadLink "https://github.com/nefarius/HidHide/releases/download/v1.5.212.0/HidHide_1.5.212_x64.exe" +#define ViGemDownloadLink "https://github.com/nefarius/ViGEmBus/releases/download/v1.22.0/ViGEmBus_1.22.0_x64_x86_arm64.exe" +#define RtssDownloadLink "https://github.com/Valkirie/HandheldCompanion/raw/main/redist/RTSSSetup736.exe" + +//Registry +#define RegAppsPath "SOFTWARE\" +MyAppSetupName+ "\" +#define SoftwareUninstallKey "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" + +#ifdef UseDotNet80 + #define MyConfigurationExt "net8.0" +#endif + +AllowNoIcons=yes +AppName={#MyAppSetupName} +AppVersion={#MyAppVersion} +AppVerName={#MyAppSetupName} +AppCopyright={#MyAppCopyright} +// remove next line if you only deploy 32-bit binaries and dependencies +ArchitecturesInstallIn64BitMode=x64 +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +CloseApplications = yes +Compression=lzma +DefaultGroupName={#MyAppSetupName} +DefaultDirName={autopf}\{#MyAppSetupName} +OutputBaseFilename={#MyBuildId}-{#MyAppVersion} +SetupIconFile="{#SourcePath}\HandheldCompanion\Resources\icon.ico" +SetupLogging=yes +MinVersion=6.0 +OutputDir={#SourcePath}\install +PrivilegesRequired=admin +SolidCompression=yes +VersionInfoVersion={#MyAppVersion} +VersionInfoCompany={#MyAppPublisher} +UninstallDisplayIcon={app}\{#MyAppExeName} + +[Languages] +Name: en; MessagesFile: "compiler:Default.isl" + +[Files] +#ifdef UseNetCoreCheck +// download netcorecheck.exe: https://go.microsoft.com/fwlink/?linkid=2135256 +// download netcorecheck_x64.exe: https://go.microsoft.com/fwlink/?linkid=2135504 +Source: "{#SourcePath}\redist\netcorecheck.exe"; Flags: dontcopy noencryption +Source: "{#SourcePath}\redist\netcorecheck_x64.exe"; Flags: dontcopy noencryption +#endif +Source: "{#SourcePath}\bin\{#MyConfiguration}\{#MyConfigurationExt}-windows10.0.19041.0\WinRing0x64.dll"; DestDir: "{app}"; Flags: onlyifdoesntexist +Source: "{#SourcePath}\bin\{#MyConfiguration}\{#MyConfigurationExt}-windows10.0.19041.0\WinRing0x64.sys"; DestDir: "{app}"; Flags: onlyifdoesntexist +Source: "{#SourcePath}\bin\{#MyConfiguration}\{#MyConfigurationExt}-windows10.0.19041.0\*"; Excludes: "*WinRing0x64.*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +Source: "{#SourcePath}\redist\SegoeIcons.ttf"; DestDir: "{autofonts}"; FontInstall: "Segoe Fluent Icons (TrueType)"; Flags: onlyifdoesntexist uninsneveruninstall +Source: "{#SourcePath}\redist\PromptFont.otf"; DestDir: "{autofonts}"; FontInstall: "PromptFont"; Flags: uninsneveruninstall + +[Icons] +Name: "{group}\{#MyAppSetupName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{group}\{cm:UninstallProgram,{#MyAppSetupName}}"; Filename: "{uninstallexe}" +Name: "{userdesktop}\{#MyAppSetupName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}" + +[Run] +Filename: "{app}\HandheldCompanion.exe"; Description:"Start Handheld Companion"; Flags: postinstall nowait shellexec skipifsilent; + +[InstallDelete] +Type: files; Name: "{userdesktop}\HidHide Configuration Client.lnk" +Type: files; Name: "{commondesktop}\HidHide Configuration Client.lnk" + +[UninstallRun] +Filename: "C:\Program Files\Nefarius Software Solutions\HidHide\x64\HidHideCLI.exe"; Parameters: "--cloak-off" ; RunOnceId: "CloakOff"; Flags: runascurrentuser runhidden + +[UninstallDelete] +Type: filesandordirs; Name: "{app}" + +[Registry] +Root: HKA; Subkey: "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps"; Flags: uninsdeletekeyifempty +Root: HKA; Subkey: "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\HandheldCompanion.exe"; ValueType: string; ValueName: "DumpFolder"; ValueData: "{userdocs}\HandheldCompanion\dumps"; Flags: uninsdeletekey + +[Code] // types and variables type + TIntegerArray = array of integer; + TDependency_Entry = record Filename: String; + NewVersion: String; + InstalledVersion: String; Parameters: String; Title: String; URL: String; Checksum: String; ForceSuccess: Boolean; - RestartAfter: Boolean; + RestartNeeded: Boolean; end; var @@ -20,7 +161,232 @@ var Dependency_NeedRestart, Dependency_ForceX86: Boolean; Dependency_DownloadPage: TDownloadWizardPage; -procedure Dependency_Add(const Filename, Parameters, Title, URL, Checksum: String; const ForceSuccess, RestartAfter: Boolean); +//Prototypes +procedure Dependency_Add(const Filename, Parameters, Title, URL, Checksum: String; const ForceSuccess: Boolean); forward; +procedure Dependency_Add_With_Version(const Filename, NewVersion, InstalledVersion, Parameters, Title, URL, Checksum: String; const ForceSuccess, RestartNeeded: Boolean); forward; +function Dependency_PrepareToInstall(var NeedsRestart: Boolean): String; forward; +function Dependency_UpdateReadyMemo(const Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String; forward; +procedure Dependency_AddDotNet80Desktop; forward; +procedure Dependency_AddDirectX; forward; +procedure Dependency_AddHideHide; forward; +procedure Dependency_AddViGem; forward; +procedure Dependency_AddRTSS; forward; +function BoolToStr(Value: Boolean): String; forward; + + +#include "./utils/CompareVersions.iss" +#include "./utils/ApiUtils.iss" +#include "./utils/RegUtils.iss" +#include "./utils/UpdateUninstallWizard.iss" +#include "./utils/Utils.iss" + + +procedure InitializeWizard; +begin + Dependency_DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil); +end; + + +procedure CurStepChanged(CurStep: TSetupStep); +begin + if CurStep = ssPostInstall then + begin + //TODO - Add firewall entry + end; +end; + + +procedure CurPageChanged(CurPageID: Integer); +begin + if CurPageID = wpFinished then + begin + if(Dependency_NeedRestart) then + WizardForm.RunList.Visible := False; + end; +end; + +function NeedRestart: Boolean; +begin + log('***Enter NeedRestart()***'); + log('NeedRestart: ' +boolToStr(Dependency_NeedRestart)); + Result := Dependency_NeedRestart; + log('!!!Leave NeedRestart()!!!'); +end; + +function PrepareToInstall(var NeedsRestart: Boolean): String; +var + PrepareToInstallResult:String; +begin + log('***Enter PrepareToInstall()***'); + log('Restart needed: ' +boolToStr(NeedsRestart)); + PrepareToInstallResult:= Dependency_PrepareToInstall(NeedsRestart); + log('Result: ' +PrepareToInstallResult); + result:= PrepareToInstallResult; + log('!!!Leave PrepareToInstall()!!!'); +end; + +function UpdateReadyMemo(const Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String; +begin + Result := Dependency_UpdateReadyMemo(Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo); +end; + + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +var + resultCode:integer; +begin + if CurUninstallStep = usUninstall then + begin + if not(checkListBox.checked[keepAllCheck]) then + begin + if DirExists(ExpandConstant('{userdocs}\{#MyBuildId}\profiles')) then + DelTree(ExpandConstant('{userdocs}\{#MyBuildId}\profiles'), True, True, True); + if DirExists(ExpandConstant('{userdocs}\{#MyBuildId}\hotkeys')) then + DelTree(ExpandConstant('{userdocs}\{#MyBuildId}\hotkeys'), True, True, True); + DelTree(ExpandConstant('{localappdata}\HandheldCompanion'), True, True, True); + exit; + end + else + begin + if not(checkListBox.checked[profilesCheck]) then + begin + if DirExists(ExpandConstant('{userdocs}\{#MyBuildId}\profiles')) then + DelTree(ExpandConstant('{userdocs}\{#MyBuildId}\profiles'), True, True, True); + end; + + if not(checkListBox.checked[hotkeysCheck]) then + begin + if DirExists(ExpandConstant('{userdocs}\{#MyBuildId}\hotkeys')) then + DelTree(ExpandConstant('{userdocs}\{#MyBuildId}\hotkeys'), True, True, True); + end; + + if not(checkListBox.checked[applicationSettingsCheck]) then + begin + DelTree(ExpandConstant('{localappdata}\HandheldCompanion'), True, True, True); + end; + end; + + if not(keepHidhideCheckbox.Checked) then + begin + uninstallHidHide(); + end; + + if not(keepVigemCheckbox.Checked) then + begin + if(ShellExec('', 'msiexec.exe', '/X{966606F3-2745-49E9-BF15-5C3EAA4E9077}', '', SW_SHOW, ewWaitUntilTerminated, resultCode)) then + begin + log('Successfully executed Vigem uninstaller'); + if(resultCode = 0) then + log('Vigem uninstaller finished successfully') + else + log('Vigem uninstaller failed with exit code ' +intToStr(resultCode)); + end + else + begin + log('Failed to execute Vigem uninstaller'); + end; + end; + end; +end; + + +function InitializeSetup: Boolean; +var + installedVersion:string; +begin + +#ifdef UseDotNet80 + installedVersion:= regGetInstalledVersion('{#DotNetName}'); + if(compareVersions('{#NewDotNetVersion}', installedVersion, '.', '-') > 0) then + begin + log('{#DotNetName} {#NewDotNetVersion} needs update.'); + Dependency_AddDotNet80Desktop; + end; +#endif + +#ifdef UseVC2005 + Dependency_AddVC2005; +#endif +#ifdef UseVC2008 + Dependency_AddVC2008; +#endif +#ifdef UseVC2010 + Dependency_AddVC2010; +#endif +#ifdef UseVC2012 + Dependency_AddVC2012; +#endif +#ifdef UseVC2013 + Dependency_AddVC2013; +#endif +#ifdef UseVC2015To2019 + Dependency_AddVC2015To2019; +#endif + +#ifdef UseDirectX + installedVersion:= regGetInstalledVersion('{#DirectXName}'); + if(compareVersions('{#NewDirectXVersion}', installedVersion, '.', '-') > 0) then + begin + log('{#DirectXName} {#NewDirectXVersion} needs update.'); + Dependency_AddDirectX; + end; +#endif + +#ifdef UseHideHide + if not(isHidHideInstalled()) then + begin + Dependency_AddHideHide; + uninstallHidHide(); + end + else + begin + installedVersion:= getInstalledHidHideVersion(); + if(compareVersions('{#NewHidHideVersion}', installedVersion, '.', '-') > 0) then + begin + log('{#HidHideName} {#NewHidHideVersion} needs update.'); + Dependency_AddHideHide; + uninstallHidHide(); + end; + end; +#endif + +#ifdef UseViGem + if not(isViGemInstalled()) then + begin + Dependency_AddViGem; + uninstallViGem(); + end + else + begin + installedVersion:= regGetInstalledVersion('{#ViGemName}'); + if(compareVersions('{#NewViGemVersion}', installedVersion, '.', '-') > 0) then + begin + log('{#ViGemName} {#NewViGemVersion} needs update.'); + Dependency_AddViGem; + uninstallViGem(); + end; + end; +#endif + +#ifdef UseRTSS + if(not isRtssInstalled()) then + Dependency_AddRTSS + else + begin + installedVersion:= getInstalledRtssVersion(); + if(compareVersions('{#NewRtssVersion}', installedVersion, '.', '-') > 0) then + begin + log('{#RtssName} {#NewRtssVersion} needs update.'); + Dependency_AddRTSS; + end; + end; +#endif + + Result := True; +end; +#endif + +procedure Dependency_Add(const Filename, Parameters, Title, URL, Checksum: String; const ForceSuccess: Boolean); var Dependency: TDependency_Entry; DependencyCount: Integer; @@ -39,18 +405,42 @@ begin Dependency.Checksum := Checksum; Dependency.ForceSuccess := ForceSuccess; - Dependency.RestartAfter := RestartAfter; DependencyCount := GetArrayLength(Dependency_List); SetArrayLength(Dependency_List, DependencyCount + 1); Dependency_List[DependencyCount] := Dependency; end; -procedure Dependency_InitializeWizard; + +procedure Dependency_Add_With_Version(const Filename, NewVersion, InstalledVersion, Parameters, Title, URL, Checksum: String; const ForceSuccess, RestartNeeded: Boolean); +var + Dependency: TDependency_Entry; + DependencyCount: Integer; begin - Dependency_DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil); + Dependency_Memo := Dependency_Memo + #13#10 + '%1' + Title; + + Dependency.Filename := Filename; + Dependency.NewVersion:= NewVersion; + Dependency.InstalledVersion:= InstalledVersion; + Dependency.Parameters := Parameters; + Dependency.Title := Title; + + if FileExists(ExpandConstant('{tmp}{\}') + Filename) then begin + Dependency.URL := ''; + end else begin + Dependency.URL := URL; + end; + + Dependency.Checksum := Checksum; + Dependency.ForceSuccess := ForceSuccess; + Dependency.RestartNeeded:= RestartNeeded; + + DependencyCount := GetArrayLength(Dependency_List); + SetArrayLength(Dependency_List, DependencyCount + 1); + Dependency_List[DependencyCount] := Dependency; end; + function Dependency_PrepareToInstall(var NeedsRestart: Boolean): String; var DependencyCount, DependencyIndex, ResultCode: Integer; @@ -95,28 +485,33 @@ begin if Result = '' then begin for DependencyIndex := 0 to DependencyCount - 1 do begin - Dependency_DownloadPage.SetText(Dependency_List[DependencyIndex].Title, ''); + Dependency_DownloadPage.SetText(Dependency_List[DependencyIndex].Title + ' ' +Dependency_List[DependencyIndex].NewVersion, ''); Dependency_DownloadPage.SetProgress(DependencyIndex + 1, DependencyCount + 1); while True do begin ResultCode := 0; - if ShellExec('', ExpandConstant('{tmp}{\}') + Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Parameters, '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode) then begin - if Dependency_List[DependencyIndex].RestartAfter then begin - if DependencyIndex = DependencyCount - 1 then begin - Dependency_NeedRestart := True; - end else begin - NeedsRestart := True; - Result := Dependency_List[DependencyIndex].Title; + if ShellExec('', ExpandConstant('{tmp}{\}') + Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Parameters, '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode) then + begin + log('Successfully executed ' +Dependency_List[DependencyIndex].Filename+ ' with result code: ' +intToStr(ResultCode)); + + if (ResultCode = 0) or Dependency_List[DependencyIndex].ForceSuccess then begin // ERROR_SUCCESS (0) + begin + if(Dependency_List[DependencyIndex].RestartNeeded) then + begin + log('Restart is needed by ' +Dependency_List[DependencyIndex].Title); + Dependency_NeedRestart := True; end; + regSetVersion(Dependency_List[DependencyIndex].Title, Dependency_List[DependencyIndex].NewVersion); break; - end else if (ResultCode = 0) or Dependency_List[DependencyIndex].ForceSuccess then begin // ERROR_SUCCESS (0) - break; + end end else if ResultCode = 1641 then begin // ERROR_SUCCESS_REBOOT_INITIATED (1641) NeedsRestart := True; + log(Dependency_List[DependencyIndex].Title + ' needs restart with result code ' +intToStr(ResultCode)); Result := Dependency_List[DependencyIndex].Title; break; end else if ResultCode = 3010 then begin // ERROR_SUCCESS_REBOOT_REQUIRED (3010) Dependency_NeedRestart := True; + log(Dependency_List[DependencyIndex].Title + ' needs restart with result code ' +intToStr(ResultCode)); break; end; end; @@ -178,6 +573,7 @@ begin end; Result := Result + FmtMessage(Dependency_Memo, [Space]); end; + log('Ready MemoResult: ' +result); end; function Dependency_IsX64: Boolean; @@ -219,14 +615,12 @@ procedure Dependency_AddDotNet80Desktop; begin // https://dotnet.microsoft.com/en-us/download/dotnet/8.0 if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App 8.0.0') then begin - Dependency_Add('dotNet80desktop' + Dependency_ArchSuffix + '.exe', + Dependency_Add_With_Version('dotNet80desktop' + Dependency_ArchSuffix + '.exe', '{#NewDotNetVersion}', regGetInstalledVersion('{#DotNetName}'), '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Desktop Runtime 8.0.0' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/b280d97f-25a9-4ab7-8a12-8291aa3af117/a37ed0e68f51fcd973e9f6cb4f40b1a7/windowsdesktop-runtime-8.0.0-win-x64.exe', - 'https://download.visualstudio.microsoft.com/download/pr/f9e3b581-059d-429f-9f0d-1d1167ff7e32/bd7661030cd5d66cd3eee0fd20b24540/windowsdesktop-runtime-8.0.0-win-x86.exe'), - '', False, False); + '{#DotNetName}', Dependency_String('{#DotNetX86DownloadLink}', '{#DotNetX64DownloadLink}'), '', False, False); end; -end; +end; + procedure Dependency_AddVC2005; begin @@ -236,7 +630,7 @@ begin '/q', 'Visual C++ 2005 Service Pack 1 Redistributable' + Dependency_ArchTitle, Dependency_String('https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x86.EXE', 'https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x64.EXE'), - '', False, False); + '', False); end; end; @@ -248,7 +642,7 @@ begin '/q', 'Visual C++ 2008 Service Pack 1 Redistributable' + Dependency_ArchTitle, Dependency_String('https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x86.exe', 'https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x64.exe'), - '', False, False); + '', False); end; end; @@ -260,7 +654,7 @@ begin '/passive /norestart', 'Visual C++ 2010 Service Pack 1 Redistributable' + Dependency_ArchTitle, Dependency_String('https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x64.exe'), - '', False, False); + '', False); end; end; @@ -272,7 +666,7 @@ begin '/passive /norestart', 'Visual C++ 2012 Update 4 Redistributable' + Dependency_ArchTitle, Dependency_String('https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe'), - '', False, False); + '', False); end; end; @@ -284,7 +678,7 @@ begin '/passive /norestart', 'Visual C++ 2013 Update 5 Redistributable' + Dependency_ArchTitle, Dependency_String('https://download.visualstudio.microsoft.com/download/pr/10912113/5da66ddebb0ad32ebd4b922fd82e8e25/vcredist_x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/10912041/cee5d6bca2ddbcd039da727bf4acb48a/vcredist_x64.exe'), - '', False, False); + '', False); end; end; @@ -296,288 +690,61 @@ begin '/passive /norestart', 'Visual C++ 2015-2019 Redistributable' + Dependency_ArchTitle, Dependency_String('https://aka.ms/vs/16/release/vc_redist.x86.exe', 'https://aka.ms/vs/16/release/vc_redist.x64.exe'), - '', False, False); + '', False); end; end; procedure Dependency_AddDirectX; begin // https://www.microsoft.com/en-US/download/details.aspx?id=35 - Dependency_Add('dxwebsetup.exe', + Dependency_Add_With_Version('dxwebsetup.exe', '{#NewDirectXVersion}', regGetInstalledVersion('{#DirectXName}'), '/q', - 'DirectX Runtime', - 'https://download.microsoft.com/download/1/7/1/1718CCC4-6315-4D8E-9543-8E28A4E18C4C/dxwebsetup.exe', + '{#DirectXName}', + '{#DirectXDownloadLink}', '', True, False); end; procedure Dependency_AddHideHide; begin - Dependency_Add('HidHide_1.4.202_x64.exe', + Dependency_Add_With_Version('HidHide_1.5.212_x64.exe', '{#NewHidHideVersion}', regGetInstalledVersion('{#HidHideName}'), '/quiet /norestart', - 'HidHide Drivers', - 'https://github.com/nefarius/HidHide/releases/download/v1.4.202.0/HidHide_1.4.202_x64.exe', + '{#HidHideName}', + '{#HidHideDownloadLink}', '', True, False); end; procedure Dependency_AddViGem; begin - Dependency_Add('ViGEmBus_1.22.0_x64_x86_arm64.exe', + Dependency_Add_With_Version('ViGEmBus_1.22.0_x64_x86_arm64.exe', '{#NewViGemVersion}', regGetInstalledVersion('{#ViGemName}'), '/quiet /norestart', - 'ViGEmBus Setup', - 'https://github.com/nefarius/ViGEmBus/releases/download/v1.22.0/ViGEmBus_1.22.0_x64_x86_arm64.exe', - '', True, False); + '{#ViGemName}', + '{#ViGemDownloadLink}', + '', True, True); end; procedure Dependency_AddRTSS; begin - Dependency_Add('RTSSSetup735Beta5.exe', + Dependency_Add_With_Version('RTSSSetup735.exe', '{#NewRtssVersion}', regGetInstalledVersion('{#RtssName}'), '/S', - 'RTSS Setup v7.3.5 Beta5', - 'https://github.com/Valkirie/HandheldCompanion/raw/main/redist/RTSSSetup735Beta5.exe', - '', True, False); -end; - -[Setup] -; ------------- -; SETUP -; ------------- -#ifndef Dependency_NoExampleSetup - -; requires netcorecheck.exe and netcorecheck_x64.exe (see download link below) -#define UseNetCoreCheck -#ifdef UseNetCoreCheck - #define UseDotNet80 -#endif - -;#define UseVC2005 -;#define UseVC2008 -;#define UseVC2010 -;#define UseVC2012 -;#define UseVC2013 -;#define UseVC2015To2019 - -#define UseDirectX -; install ViGem first -#define UseViGem -#define UseHideHide -#define UseRTSS - -#define MyAppSetupName 'Handheld Companion' -#define MyBuildId 'HandheldCompanion' -#define MyAppVersion '0.20.4.1' -#define MyAppPublisher 'BenjaminLSR' -#define MyAppCopyright 'Copyright @ BenjaminLSR' -#define MyAppURL 'https://github.com/Valkirie/HandheldCompanion' -#define MyAppExeName "HandheldCompanion.exe" -#define MyConfiguration "Release" - -#ifdef UseDotNet80 - #define MyConfigurationExt "net8.0" -#endif - -AppName={#MyAppSetupName} -AppVersion={#MyAppVersion} -AppVerName={#MyAppSetupName} -AppCopyright={#MyAppCopyright} -VersionInfoVersion={#MyAppVersion} -VersionInfoCompany={#MyAppPublisher} -AppPublisher={#MyAppPublisher} -AppPublisherURL={#MyAppURL} -AppSupportURL={#MyAppURL} -AppUpdatesURL={#MyAppURL} -OutputBaseFilename={#MyBuildId}-{#MyAppVersion} -DefaultGroupName={#MyAppSetupName} -DefaultDirName={autopf}\{#MyAppSetupName} -UninstallDisplayIcon={app}\{#MyAppExeName} -SetupIconFile="{#SourcePath}\HandheldCompanion\Resources\icon.ico" -SourceDir=redist -OutputDir={#SourcePath}\install -AllowNoIcons=yes -MinVersion=6.0 -;PrivilegesRequired=admin -PrivilegesRequiredOverridesAllowed=dialog -Compression=lzma -SolidCompression=yes - -// remove next line if you only deploy 32-bit binaries and dependencies -ArchitecturesInstallIn64BitMode=x64 - -[Languages] -Name: en; MessagesFile: "compiler:Default.isl" - -[Setup] -AlwaysRestart = yes -CloseApplications = yes - -[Files] -#ifdef UseNetCoreCheck -// download netcorecheck.exe: https://go.microsoft.com/fwlink/?linkid=2135256 -// download netcorecheck_x64.exe: https://go.microsoft.com/fwlink/?linkid=2135504 -Source: "netcorecheck.exe"; Flags: dontcopy noencryption -Source: "netcorecheck_x64.exe"; Flags: dontcopy noencryption -#endif - -Source: "{#SourcePath}\bin\{#MyConfiguration}\{#MyConfigurationExt}-windows10.0.19041.0\WinRing0x64.dll"; DestDir: "{app}"; Flags: onlyifdoesntexist -Source: "{#SourcePath}\bin\{#MyConfiguration}\{#MyConfigurationExt}-windows10.0.19041.0\WinRing0x64.sys"; DestDir: "{app}"; Flags: onlyifdoesntexist -Source: "{#SourcePath}\bin\{#MyConfiguration}\{#MyConfigurationExt}-windows10.0.19041.0\*"; Excludes: "*WinRing0x64.*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs - -Source: "{#SourcePath}\redist\SegoeIcons.ttf"; DestDir: "{autofonts}"; FontInstall: "Segoe Fluent Icons (TrueType)"; Flags: onlyifdoesntexist uninsneveruninstall -Source: "{#SourcePath}\redist\PromptFont.otf"; DestDir: "{autofonts}"; FontInstall: "PromptFont"; Flags: uninsneveruninstall - -[Icons] -Name: "{group}\{#MyAppSetupName}"; Filename: "{app}\{#MyAppExeName}" -Name: "{group}\{cm:UninstallProgram,{#MyAppSetupName}}"; Filename: "{uninstallexe}" -Name: "{commondesktop}\{#MyAppSetupName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon - -[Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}" - -[UninstallRun] -Filename: "C:\Program Files\Nefarius Software Solutions\HidHide\x64\HidHideCLI.exe"; Parameters: "--cloak-off" ; RunOnceId: "CloakOff"; Flags: runascurrentuser runhidden - -[UninstallDelete] -Type: filesandordirs; Name: "{app}" - -[Registry] -Root: HKLM; Subkey: "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps"; Flags: uninsdeletekeyifempty -Root: HKLM; Subkey: "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\HandheldCompanion.exe"; ValueType: string; ValueName: "DumpFolder"; ValueData: "{userdocs}\HandheldCompanion\dumps"; Flags: uninsdeletekey - -[Code] -#include "./UpdateUninstallWizard.iss" - -procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); -var - resultCode:integer; -begin - if CurUninstallStep = usUninstall then - begin - if not(checkListBox.checked[keepAllCheck]) then - begin - if DirExists(ExpandConstant('{userdocs}\{#MyBuildId}\profiles')) then - DelTree(ExpandConstant('{userdocs}\{#MyBuildId}\profiles'), True, True, True); - if DirExists(ExpandConstant('{userdocs}\{#MyBuildId}\hotkeys')) then - DelTree(ExpandConstant('{userdocs}\{#MyBuildId}\hotkeys'), True, True, True); - DelTree(ExpandConstant('{localappdata}\HandheldCompanion'), True, True, True); - exit; - end - else - begin - if not(checkListBox.checked[profilesCheck]) then - begin - if DirExists(ExpandConstant('{userdocs}\{#MyBuildId}\profiles')) then - DelTree(ExpandConstant('{userdocs}\{#MyBuildId}\profiles'), True, True, True); - end; - - if not(checkListBox.checked[hotkeysCheck]) then - begin - if DirExists(ExpandConstant('{userdocs}\{#MyBuildId}\hotkeys')) then - DelTree(ExpandConstant('{userdocs}\{#MyBuildId}\hotkeys'), True, True, True); - end; - - if not(checkListBox.checked[applicationSettingsCheck]) then - begin - DelTree(ExpandConstant('{localappdata}\HandheldCompanion'), True, True, True); - end; - end; - - if not(keepHidhideCheckbox.Checked) then - begin - if(ShellExec('', 'msiexec.exe', '/X{41DC2CF5-D952-4EC5-B90B-136E59430EA0} /L*V "C:\HidHide-Uninstall.log"', '', SW_SHOW, ewWaitUntilTerminated, resultCode)) then - begin - log('Successfully executed Hidhide uninstaller'); - if(resultCode = 0) then - log('Hidhide uninstaller finished successfully') - else - log('Hidhide uninstaller failed with exit code ' +intToStr(resultCode)); - end - else - begin - log('Failed to execute Hidhide uninstaller'); - end; - end; - - if not(keepVigemCheckbox.Checked) then - begin - if(ShellExec('', 'msiexec.exe', '/X{966606F3-2745-49E9-BF15-5C3EAA4E9077}', '', SW_SHOW, ewWaitUntilTerminated, resultCode)) then - begin - log('Successfully executed Vigem uninstaller'); - if(resultCode = 0) then - log('Vigem uninstaller finished successfully') - else - log('Vigem uninstaller failed with exit code ' +intToStr(resultCode)); - end - else - begin - log('Failed to execute Vigem uninstaller'); - end; - end; + '{#RtssName}', + '{#RtssDownloadLink}', + '', True, False); + + stopProcess('{#EncoderServer64Exe}'); + stopProcess('{#RTSSHooksLoader64Exe}'); + stopProcess('{#EncoderServerExe}'); + stopProcess('{#RTSSHooksLoaderExe}'); + + if(isProcessRunning('{#RtssExe}')) then + begin + stopProcess('{#RtssExe}'); end; end; - - -procedure InitializeWizard; -begin - Dependency_InitializeWizard; -end; - -function PrepareToInstall(var NeedsRestart: Boolean): String; + +function BoolToStr(Value: Boolean): String; begin - Result := Dependency_PrepareToInstall(NeedsRestart); -end; - -function NeedRestart: Boolean; -begin - Result := Dependency_NeedRestart; -end; - -function UpdateReadyMemo(const Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String; -begin - Result := Dependency_UpdateReadyMemo(Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo); + if Value then + Result := 'Yes' + else + Result := 'No'; end; - -function InitializeSetup: Boolean; -begin - -#ifdef UseDotNet80 - Dependency_AddDotNet80Desktop; -#endif - -#ifdef UseVC2005 - Dependency_AddVC2005; -#endif -#ifdef UseVC2008 - Dependency_AddVC2008; -#endif -#ifdef UseVC2010 - Dependency_AddVC2010; -#endif -#ifdef UseVC2012 - Dependency_AddVC2012; -#endif -#ifdef UseVC2013 - Dependency_AddVC2013; -#endif -#ifdef UseVC2015To2019 - Dependency_AddVC2015To2019; -#endif - -#ifdef UseDirectX - Dependency_AddDirectX; -#endif - -#ifdef UseHideHide - Dependency_AddHideHide; -#endif - -#ifdef UseViGem - Dependency_AddViGem; -#endif - -#ifdef UseRTSS - Dependency_AddRTSS; -#endif - - Result := True; -end; - -#endif diff --git a/HandheldCompanion/ADLX/ADLXBackend.cs b/HandheldCompanion/ADLX/ADLXBackend.cs index b0353e79e..64d3ca68d 100644 --- a/HandheldCompanion/ADLX/ADLXBackend.cs +++ b/HandheldCompanion/ADLX/ADLXBackend.cs @@ -1,134 +1,134 @@ -using System.Runtime.InteropServices; -using System.Text; - -namespace HandheldCompanion.ADLX -{ - public class ADLXBackend - { - public const string ADLX_Wrapper = @"ADLX_Wrapper.dll"; - - [StructLayout(LayoutKind.Sequential)] - public struct AdlxTelemetryData - { - // GPU Usage - public bool gpuUsageSupported; - public double gpuUsageValue; - - // GPU Core Frequency - public bool gpuClockSpeedSupported; - public double gpuClockSpeedValue; - - // GPU VRAM Frequency - public bool gpuVRAMClockSpeedSupported; - public double gpuVRAMClockSpeedValue; - - // GPU Core Temperature - public bool gpuTemperatureSupported; - public double gpuTemperatureValue; - - // GPU Hotspot Temperature - public bool gpuHotspotTemperatureSupported; - public double gpuHotspotTemperatureValue; - - // GPU Power - public bool gpuPowerSupported; - public double gpuPowerValue; - - // Fan Speed - public bool gpuFanSpeedSupported; - public double gpuFanSpeedValue; - - // VRAM Usage - public bool gpuVramSupported; - public double gpuVramValue; - - // GPU Voltage - public bool gpuVoltageSupported; - public double gpuVoltageValue; - - // GPU TBP - public bool gpuTotalBoardPowerSupported; - public double gpuTotalBoardPowerValue; - } - - public enum ADLX_RESULT - { - ADLX_OK = 0, /**< @ENG_START_DOX This result indicates success. @ENG_END_DOX */ - ADLX_ALREADY_ENABLED, /**< @ENG_START_DOX This result indicates that the asked action is already enabled. @ENG_END_DOX */ - ADLX_ALREADY_INITIALIZED, /**< @ENG_START_DOX This result indicates that ADLX has a unspecified type of initialization. @ENG_END_DOX */ - ADLX_FAIL, /**< @ENG_START_DOX This result indicates an unspecified failure. @ENG_END_DOX */ - ADLX_INVALID_ARGS, /**< @ENG_START_DOX This result indicates that the arguments are invalid. @ENG_END_DOX */ - ADLX_BAD_VER, /**< @ENG_START_DOX This result indicates that the asked version is incompatible with the current version. @ENG_END_DOX */ - ADLX_UNKNOWN_INTERFACE, /**< @ENG_START_DOX This result indicates that an unknown interface was asked. @ENG_END_DOX */ - ADLX_TERMINATED, /**< @ENG_START_DOX This result indicates that the calls were made in an interface after ADLX was terminated. @ENG_END_DOX */ - ADLX_ADL_INIT_ERROR, /**< @ENG_START_DOX This result indicates that the ADL initialization failed. @ENG_END_DOX */ - ADLX_NOT_FOUND, /**< @ENG_START_DOX This result indicates that the item is not found. @ENG_END_DOX */ - ADLX_INVALID_OBJECT, /**< @ENG_START_DOX This result indicates that the method was called into an invalid object. @ENG_END_DOX */ - ADLX_ORPHAN_OBJECTS, /**< @ENG_START_DOX This result indicates that ADLX was terminated with outstanding ADLX objects. Any interface obtained from ADLX points to invalid memory and calls in their methods will result in unexpected behavior. @ENG_END_DOX */ - ADLX_NOT_SUPPORTED, /**< @ENG_START_DOX This result indicates that the asked feature is not supported. @ENG_END_DOX */ - ADLX_PENDING_OPERATION, /**< @ENG_START_DOX This result indicates a failure due to an operation currently in progress. @ENG_END_DOX */ - ADLX_GPU_INACTIVE /**< @ENG_START_DOX This result indicates that the GPU is inactive. @ENG_END_DOX */ - } - - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool IntializeAdlx(); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool CloseAdlx(); - - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern ADLX_RESULT GetNumberOfDisplays(ref int displayNum); - - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] - public static extern ADLX_RESULT GetDisplayName(int idx, StringBuilder dispName, int nameLength); - - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] - public static extern ADLX_RESULT GetDisplayGPU(int idx, ref int UniqueId); - - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] - public static extern ADLX_RESULT GetGPUIndex(int UniqueId, ref int idx); - - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool HasRSRSupport(); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetRSR(); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetRSR(bool enable); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern int GetRSRSharpness(); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetRSRSharpness(int sharpness); - - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetAntiLag(int GPU); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetAntiLag(int GPU, bool enable); - - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetBoost(int GPU); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetBoost(int GPU, bool enable); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern int GetBoostResolution(int GPU); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetBoostResolution(int GPU, int minRes); - - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetChill(int GPU); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetChill(int GPU, bool enable); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern int GetChillMinFPS(int GPU); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetChillMinFPS(int GPU, int minFPS); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern int GetChillMaxFPS(int GPU); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetChillMaxFPS(int GPU, int maxFPS); - - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetImageSharpening(int GPU); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetImageSharpening(int GPU, bool enable); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern int GetImageSharpeningSharpness(int GPU); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetImageSharpeningSharpness(int GPU, int sharpness); - - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool HasIntegerScalingSupport(int displayIdx); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetIntegerScaling(int displayIdx); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetIntegerScaling(int displayIdx, bool enabled); - - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool HasGPUScalingSupport(int displayIdx); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetGPUScaling(int displayIdx); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetGPUScaling(int displayIdx, bool enabled); - - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool HasScalingModeSupport(int displayIdx); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern int GetScalingMode(int displayIdx); - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetScalingMode(int displayIdx, int mode); - - [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetAdlxTelemetry(int GPU, ref AdlxTelemetryData adlxTelemetryData); - - internal static AdlxTelemetryData GetTelemetryData() - { - AdlxTelemetryData TelemetryData = new(); - bool Result = GetAdlxTelemetry(0, ref TelemetryData); - return TelemetryData; - } - } -} +using System.Runtime.InteropServices; +using System.Text; + +namespace HandheldCompanion.ADLX +{ + public class ADLXBackend + { + public const string ADLX_Wrapper = @"ADLX_Wrapper.dll"; + + [StructLayout(LayoutKind.Sequential)] + public struct AdlxTelemetryData + { + // GPU Usage + public bool gpuUsageSupported; + public double gpuUsageValue; + + // GPU Core Frequency + public bool gpuClockSpeedSupported; + public double gpuClockSpeedValue; + + // GPU VRAM Frequency + public bool gpuVRAMClockSpeedSupported; + public double gpuVRAMClockSpeedValue; + + // GPU Core Temperature + public bool gpuTemperatureSupported; + public double gpuTemperatureValue; + + // GPU Hotspot Temperature + public bool gpuHotspotTemperatureSupported; + public double gpuHotspotTemperatureValue; + + // GPU Power + public bool gpuPowerSupported; + public double gpuPowerValue; + + // Fan Speed + public bool gpuFanSpeedSupported; + public double gpuFanSpeedValue; + + // VRAM Usage + public bool gpuVramSupported; + public double gpuVramValue; + + // GPU Voltage + public bool gpuVoltageSupported; + public double gpuVoltageValue; + + // GPU TBP + public bool gpuTotalBoardPowerSupported; + public double gpuTotalBoardPowerValue; + } + + public enum ADLX_RESULT + { + ADLX_OK = 0, /**< @ENG_START_DOX This result indicates success. @ENG_END_DOX */ + ADLX_ALREADY_ENABLED, /**< @ENG_START_DOX This result indicates that the asked action is already enabled. @ENG_END_DOX */ + ADLX_ALREADY_INITIALIZED, /**< @ENG_START_DOX This result indicates that ADLX has a unspecified type of initialization. @ENG_END_DOX */ + ADLX_FAIL, /**< @ENG_START_DOX This result indicates an unspecified failure. @ENG_END_DOX */ + ADLX_INVALID_ARGS, /**< @ENG_START_DOX This result indicates that the arguments are invalid. @ENG_END_DOX */ + ADLX_BAD_VER, /**< @ENG_START_DOX This result indicates that the asked version is incompatible with the current version. @ENG_END_DOX */ + ADLX_UNKNOWN_INTERFACE, /**< @ENG_START_DOX This result indicates that an unknown interface was asked. @ENG_END_DOX */ + ADLX_TERMINATED, /**< @ENG_START_DOX This result indicates that the calls were made in an interface after ADLX was terminated. @ENG_END_DOX */ + ADLX_ADL_INIT_ERROR, /**< @ENG_START_DOX This result indicates that the ADL initialization failed. @ENG_END_DOX */ + ADLX_NOT_FOUND, /**< @ENG_START_DOX This result indicates that the item is not found. @ENG_END_DOX */ + ADLX_INVALID_OBJECT, /**< @ENG_START_DOX This result indicates that the method was called into an invalid object. @ENG_END_DOX */ + ADLX_ORPHAN_OBJECTS, /**< @ENG_START_DOX This result indicates that ADLX was terminated with outstanding ADLX objects. Any interface obtained from ADLX points to invalid memory and calls in their methods will result in unexpected behavior. @ENG_END_DOX */ + ADLX_NOT_SUPPORTED, /**< @ENG_START_DOX This result indicates that the asked feature is not supported. @ENG_END_DOX */ + ADLX_PENDING_OPERATION, /**< @ENG_START_DOX This result indicates a failure due to an operation currently in progress. @ENG_END_DOX */ + ADLX_GPU_INACTIVE /**< @ENG_START_DOX This result indicates that the GPU is inactive. @ENG_END_DOX */ + } + + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool IntializeAdlx(); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool CloseAdlx(); + + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern ADLX_RESULT GetNumberOfDisplays(ref int displayNum); + + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + public static extern ADLX_RESULT GetDisplayName(int idx, StringBuilder dispName, int nameLength); + + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] + public static extern ADLX_RESULT GetDisplayGPU(int idx, ref int UniqueId); + + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] + public static extern ADLX_RESULT GetGPUIndex(int UniqueId, ref int idx); + + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool HasRSRSupport(); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetRSR(); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetRSR(bool enable); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern int GetRSRSharpness(); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetRSRSharpness(int sharpness); + + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetAntiLag(int GPU); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetAntiLag(int GPU, bool enable); + + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetBoost(int GPU); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetBoost(int GPU, bool enable); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern int GetBoostResolution(int GPU); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetBoostResolution(int GPU, int minRes); + + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetChill(int GPU); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetChill(int GPU, bool enable); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern int GetChillMinFPS(int GPU); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetChillMinFPS(int GPU, int minFPS); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern int GetChillMaxFPS(int GPU); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetChillMaxFPS(int GPU, int maxFPS); + + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetImageSharpening(int GPU); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetImageSharpening(int GPU, bool enable); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern int GetImageSharpeningSharpness(int GPU); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetImageSharpeningSharpness(int GPU, int sharpness); + + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool HasIntegerScalingSupport(int displayIdx); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetIntegerScaling(int displayIdx); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetIntegerScaling(int displayIdx, bool enabled); + + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool HasGPUScalingSupport(int displayIdx); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetGPUScaling(int displayIdx); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetGPUScaling(int displayIdx, bool enabled); + + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool HasScalingModeSupport(int displayIdx); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern int GetScalingMode(int displayIdx); + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool SetScalingMode(int displayIdx, int mode); + + [DllImport(ADLX_Wrapper, CallingConvention = CallingConvention.Cdecl)] public static extern bool GetAdlxTelemetry(int GPU, ref AdlxTelemetryData adlxTelemetryData); + + internal static AdlxTelemetryData GetTelemetryData() + { + AdlxTelemetryData TelemetryData = new(); + bool Result = GetAdlxTelemetry(0, ref TelemetryData); + return TelemetryData; + } + } +} diff --git a/HandheldCompanion/Actions/GyroActions.cs b/HandheldCompanion/Actions/GyroActions.cs index 7b75a9478..b889ba6a6 100644 --- a/HandheldCompanion/Actions/GyroActions.cs +++ b/HandheldCompanion/Actions/GyroActions.cs @@ -1,32 +1,32 @@ -using HandheldCompanion.Inputs; -using HandheldCompanion.Utils; -using System; - -namespace HandheldCompanion.Actions -{ - [Serializable] - public class GyroActions : IActions - { - public MotionInput MotionInput = MotionInput.LocalSpace; - public MotionMode MotionMode = MotionMode.Off; - public bool MotionToggleStatus = false; - public bool MotionTogglePressed = false; // for debouncing - - public ButtonState MotionTrigger = new(); - - public float gyroWeight = DefaultGyroWeight; - - // const vars - public const int DefaultAxisAntiDeadZone = 15; - public const AxisLayoutFlags DefaultAxisLayoutFlags = AxisLayoutFlags.RightStick; - - public const MouseActionsType DefaultMouseActionsType = MouseActionsType.Move; - public const int DefaultSensivity = 33; - public const int DefaultDeadzone = 10; - public const float DefaultGyroWeight = 1.2f; - - public GyroActions() - { - } - } -} +using HandheldCompanion.Inputs; +using HandheldCompanion.Utils; +using System; + +namespace HandheldCompanion.Actions +{ + [Serializable] + public class GyroActions : IActions + { + public MotionInput MotionInput = MotionInput.LocalSpace; + public MotionMode MotionMode = MotionMode.Off; + public bool MotionToggleStatus = false; + public bool MotionTogglePressed = false; // for debouncing + + public ButtonState MotionTrigger = new(); + + public float gyroWeight = DefaultGyroWeight; + + // const vars + public const int DefaultAxisAntiDeadZone = 15; + public const AxisLayoutFlags DefaultAxisLayoutFlags = AxisLayoutFlags.RightStick; + + public const MouseActionsType DefaultMouseActionsType = MouseActionsType.Move; + public const int DefaultSensivity = 33; + public const int DefaultDeadzone = 10; + public const float DefaultGyroWeight = 1.2f; + + public GyroActions() + { + } + } +} diff --git a/HandheldCompanion/Actions/MouseActions.cs b/HandheldCompanion/Actions/MouseActions.cs index ae20b4839..97fdc8c00 100644 --- a/HandheldCompanion/Actions/MouseActions.cs +++ b/HandheldCompanion/Actions/MouseActions.cs @@ -1,206 +1,206 @@ -using HandheldCompanion.Inputs; -using HandheldCompanion.Simulators; -using System; -using System.ComponentModel; -using System.Numerics; -using WindowsInput.Events; - -namespace HandheldCompanion.Actions -{ - [Serializable] - public enum MouseActionsType - { - [Description("Left Button")] - LeftButton = 1, - [Description("Right Button")] - RightButton = 2, - [Description("Middle Button")] - MiddleButton = 3, - - [Description("Move Cursor")] - Move = 4, - [Description("Scroll Wheel")] - Scroll = 5, - - [Description("Scroll Up")] - ScrollUp = 6, - [Description("Scroll Down")] - ScrollDown = 7, - } - - [Serializable] - public class MouseActions : GyroActions - { - public MouseActionsType MouseType; - - // const settings - private const int scrollAmountInClicks = 20; - private const float FilterBeta = 0.5f; - - // runtime variables - private bool IsCursorDown = false; - private bool IsTouched = false; - private Vector2 remainder = new(); - private KeyCode[] pressed; - private OneEuroFilterPair mouseFilter; - private Vector2 prevVector = new(); - - // settings click - public ModifierSet Modifiers = ModifierSet.None; - - // settings axis - public int Sensivity = 33; - public float Acceleration = 1.0f; - public int Deadzone = 10; // stick only - public bool Filtering = false; // pad only - public float FilterCutoff = 0.05f; // pad only - public bool AxisRotated = false; - public bool AxisInverted = false; - - public MouseActions() - { - this.actionType = ActionType.Mouse; - - this.Value = false; - this.prevValue = false; - - mouseFilter = new(FilterCutoff, FilterBeta); - } - - public MouseActions(MouseActionsType type) : this() - { - this.MouseType = type; - } - - public override void Execute(ButtonFlags button, bool value) - { - base.Execute(button, value); - - switch (this.Value) - { - case true: - { - if (IsCursorDown) - return; - - IsCursorDown = true; - pressed = ModifierMap[Modifiers]; - KeyboardSimulator.KeyDown(pressed); - MouseSimulator.MouseDown(MouseType, scrollAmountInClicks); - SetHaptic(button, false); - } - break; - case false: - { - if (!IsCursorDown) - return; - - IsCursorDown = false; - MouseSimulator.MouseUp(MouseType); - KeyboardSimulator.KeyUp(pressed); - SetHaptic(button, true); - } - break; - } - } - - private bool IsNewTouch(bool value) - { - if (value == IsTouched) - return false; - if (IsTouched = value) - return true; - return false; - } - - public void Execute(AxisLayout layout, bool touched) - { - // this line needs to be before the next vector zero check - bool newTouch = IsNewTouch(touched); - - if (layout.vector == Vector2.Zero) - return; - - layout.vector.Y *= -1; - - Vector2 deltaVector; - float sensitivityFinetune; - - switch (layout.flags) - { - case AxisLayoutFlags.LeftStick: - case AxisLayoutFlags.RightStick: - case AxisLayoutFlags.Gyroscope: - default: - { - // convert to <0.0-1.0> values - deltaVector = layout.vector / short.MaxValue; - float deadzone = Deadzone / 100.0f; - - // apply deadzone - if (deltaVector.Length() < deadzone) - return; - - deltaVector *= (deltaVector.Length() - deadzone) / deltaVector.Length(); // shorten by deadzone - deltaVector *= 1.0f / (1.0f - deadzone); // rescale to 0.0 - 1.0 - - sensitivityFinetune = (MouseType == MouseActionsType.Move ? 0.3f : 0.1f); - } - break; - - case AxisLayoutFlags.LeftPad: - case AxisLayoutFlags.RightPad: - { - // touchpad was touched, update entry point for delta calculations - if (newTouch) - { - prevVector = layout.vector; - return; - } - - // calculate delta and convert to <0.0-1.0> values - deltaVector = (layout.vector - prevVector) / short.MaxValue; - prevVector = layout.vector; - - sensitivityFinetune = (MouseType == MouseActionsType.Move ? 9.0f : 3.0f); - } - break; - } - - if (Filtering) - { - mouseFilter.SetFilterCutoff(FilterCutoff); - deltaVector.X = (float)mouseFilter.axis1Filter.Filter(deltaVector.X, 1); - deltaVector.Y = (float)mouseFilter.axis2Filter.Filter(deltaVector.Y, 1); - } - - if (Acceleration != 1.0f) - { - deltaVector.X = (float)(Math.Sign(deltaVector.X) * Math.Pow(Math.Abs(deltaVector.X), Acceleration)); - deltaVector.Y = (float)(Math.Sign(deltaVector.Y) * Math.Pow(Math.Abs(deltaVector.Y), Acceleration)); - sensitivityFinetune = (float)Math.Pow(sensitivityFinetune, Acceleration); - } - - // apply sensitivity, rotation and slider finetune - deltaVector *= Sensivity * sensitivityFinetune; - if (AxisRotated) - deltaVector = new(-deltaVector.Y, deltaVector.X); - deltaVector *= (AxisInverted ? -1.0f : 1.0f); - - // handle the fact that MoveBy()/*Scroll() are int only and we can have movement (0 < abs(delta) < 1) - deltaVector += remainder; // add partial previous step - Vector2 intVector = new((int)Math.Truncate(deltaVector.X), (int)Math.Truncate(deltaVector.Y)); - remainder = deltaVector - intVector; // and save the unused rest - - if (MouseType == MouseActionsType.Move) - { - MouseSimulator.MoveBy((int)intVector.X, (int)intVector.Y); - } - else /* if (MouseType == MouseActionsType.Scroll) */ - { - // MouseSimulator.HorizontalScroll((int)-intVector.X); - MouseSimulator.VerticalScroll((int)-intVector.Y); - } - } - } -} +using HandheldCompanion.Inputs; +using HandheldCompanion.Simulators; +using System; +using System.ComponentModel; +using System.Numerics; +using WindowsInput.Events; + +namespace HandheldCompanion.Actions +{ + [Serializable] + public enum MouseActionsType + { + [Description("Left Button")] + LeftButton = 1, + [Description("Right Button")] + RightButton = 2, + [Description("Middle Button")] + MiddleButton = 3, + + [Description("Move Cursor")] + Move = 4, + [Description("Scroll Wheel")] + Scroll = 5, + + [Description("Scroll Up")] + ScrollUp = 6, + [Description("Scroll Down")] + ScrollDown = 7, + } + + [Serializable] + public class MouseActions : GyroActions + { + public MouseActionsType MouseType; + + // const settings + private const int scrollAmountInClicks = 20; + private const float FilterBeta = 0.5f; + + // runtime variables + private bool IsCursorDown = false; + private bool IsTouched = false; + private Vector2 remainder = new(); + private KeyCode[] pressed; + private OneEuroFilterPair mouseFilter; + private Vector2 prevVector = new(); + + // settings click + public ModifierSet Modifiers = ModifierSet.None; + + // settings axis + public int Sensivity = 33; + public float Acceleration = 1.0f; + public int Deadzone = 10; // stick only + public bool Filtering = false; // pad only + public float FilterCutoff = 0.05f; // pad only + public bool AxisRotated = false; + public bool AxisInverted = false; + + public MouseActions() + { + this.actionType = ActionType.Mouse; + + this.Value = false; + this.prevValue = false; + + mouseFilter = new(FilterCutoff, FilterBeta); + } + + public MouseActions(MouseActionsType type) : this() + { + this.MouseType = type; + } + + public override void Execute(ButtonFlags button, bool value) + { + base.Execute(button, value); + + switch (this.Value) + { + case true: + { + if (IsCursorDown) + return; + + IsCursorDown = true; + pressed = ModifierMap[Modifiers]; + KeyboardSimulator.KeyDown(pressed); + MouseSimulator.MouseDown(MouseType, scrollAmountInClicks); + SetHaptic(button, false); + } + break; + case false: + { + if (!IsCursorDown) + return; + + IsCursorDown = false; + MouseSimulator.MouseUp(MouseType); + KeyboardSimulator.KeyUp(pressed); + SetHaptic(button, true); + } + break; + } + } + + private bool IsNewTouch(bool value) + { + if (value == IsTouched) + return false; + if (IsTouched = value) + return true; + return false; + } + + public void Execute(AxisLayout layout, bool touched) + { + // this line needs to be before the next vector zero check + bool newTouch = IsNewTouch(touched); + + if (layout.vector == Vector2.Zero) + return; + + layout.vector.Y *= -1; + + Vector2 deltaVector; + float sensitivityFinetune; + + switch (layout.flags) + { + case AxisLayoutFlags.LeftStick: + case AxisLayoutFlags.RightStick: + case AxisLayoutFlags.Gyroscope: + default: + { + // convert to <0.0-1.0> values + deltaVector = layout.vector / short.MaxValue; + float deadzone = Deadzone / 100.0f; + + // apply deadzone + if (deltaVector.Length() < deadzone) + return; + + deltaVector *= (deltaVector.Length() - deadzone) / deltaVector.Length(); // shorten by deadzone + deltaVector *= 1.0f / (1.0f - deadzone); // rescale to 0.0 - 1.0 + + sensitivityFinetune = (MouseType == MouseActionsType.Move ? 0.3f : 0.1f); + } + break; + + case AxisLayoutFlags.LeftPad: + case AxisLayoutFlags.RightPad: + { + // touchpad was touched, update entry point for delta calculations + if (newTouch) + { + prevVector = layout.vector; + return; + } + + // calculate delta and convert to <0.0-1.0> values + deltaVector = (layout.vector - prevVector) / short.MaxValue; + prevVector = layout.vector; + + sensitivityFinetune = (MouseType == MouseActionsType.Move ? 9.0f : 3.0f); + } + break; + } + + if (Filtering) + { + mouseFilter.SetFilterCutoff(FilterCutoff); + deltaVector.X = (float)mouseFilter.axis1Filter.Filter(deltaVector.X, 1); + deltaVector.Y = (float)mouseFilter.axis2Filter.Filter(deltaVector.Y, 1); + } + + if (Acceleration != 1.0f) + { + deltaVector.X = (float)(Math.Sign(deltaVector.X) * Math.Pow(Math.Abs(deltaVector.X), Acceleration)); + deltaVector.Y = (float)(Math.Sign(deltaVector.Y) * Math.Pow(Math.Abs(deltaVector.Y), Acceleration)); + sensitivityFinetune = (float)Math.Pow(sensitivityFinetune, Acceleration); + } + + // apply sensitivity, rotation and slider finetune + deltaVector *= Sensivity * sensitivityFinetune; + if (AxisRotated) + deltaVector = new(-deltaVector.Y, deltaVector.X); + deltaVector *= (AxisInverted ? -1.0f : 1.0f); + + // handle the fact that MoveBy()/*Scroll() are int only and we can have movement (0 < abs(delta) < 1) + deltaVector += remainder; // add partial previous step + Vector2 intVector = new((int)Math.Truncate(deltaVector.X), (int)Math.Truncate(deltaVector.Y)); + remainder = deltaVector - intVector; // and save the unused rest + + if (MouseType == MouseActionsType.Move) + { + MouseSimulator.MoveBy((int)intVector.X, (int)intVector.Y); + } + else /* if (MouseType == MouseActionsType.Scroll) */ + { + // MouseSimulator.HorizontalScroll((int)-intVector.X); + MouseSimulator.VerticalScroll((int)-intVector.Y); + } + } + } +} diff --git a/HandheldCompanion/Actions/SpecialActions.cs b/HandheldCompanion/Actions/SpecialActions.cs deleted file mode 100644 index 3e1bd7f61..000000000 --- a/HandheldCompanion/Actions/SpecialActions.cs +++ /dev/null @@ -1,58 +0,0 @@ -using HandheldCompanion.Inputs; -using HandheldCompanion.Misc; -using HandheldCompanion.Simulators; -using System; -using System.ComponentModel; -using System.Numerics; - -namespace HandheldCompanion.Actions -{ - [Serializable] - public enum SpecialActionsType - { - [Description("Flick Stick")] - FlickStick = 0, - } - - [Serializable] - public class SpecialActions : IActions - { - public SpecialActionsType SpecialType; - - // runtime variables - private FlickStick flickStick = new(); - private float remainder = 0; - - // settings - public float FlickSensitivity = 5.0f; - public float SweepSensitivity = 5.0f; - public float FlickThreshold = 0.75f; - public int FlickSpeed = 100; - public int FlickFrontAngleDeadzone = 15; - - public SpecialActions() - { - this.actionType = ActionType.Special; - } - - public SpecialActions(SpecialActionsType type) : this() - { - this.SpecialType = type; - } - - public void Execute(AxisLayout layout) - { - if (layout.vector == Vector2.Zero) - return; - - float delta = flickStick.Handle(layout.vector, FlickSensitivity, SweepSensitivity, - FlickThreshold, FlickSpeed, FlickFrontAngleDeadzone); - - delta += remainder; - int intDelta = (int)Math.Truncate(delta); - remainder = delta - intDelta; - - MouseSimulator.MoveBy(intDelta, 0); - } - } -} diff --git a/HandheldCompanion/App.xaml.cs b/HandheldCompanion/App.xaml.cs index 53a794076..dc1168cf4 100644 --- a/HandheldCompanion/App.xaml.cs +++ b/HandheldCompanion/App.xaml.cs @@ -1,130 +1,130 @@ -using HandheldCompanion.Managers; -using HandheldCompanion.Utils; -using HandheldCompanion.Views; -using System; -using System.Diagnostics; -using System.Globalization; -using System.Reflection; -using System.Threading; -using System.Windows; -using static HandheldCompanion.WinAPI; - -namespace HandheldCompanion; - -/// -/// Interaction logic for App.xaml -/// -public partial class App : Application -{ - /// - /// Initializes the singleton application object. This is the first line of authored code - /// executed, and as such is the logical equivalent of main() or WinMain(). - /// - public App() - { - InitializeComponent(); - } - - /// - /// Invoked when the application is launched normally by the end user. Other entry points - /// will be used such as when the application is launched to open a specific file. - /// - /// Details about the launch request and process. - protected override void OnStartup(StartupEventArgs args) - { - // get current assembly - var CurrentAssembly = Assembly.GetExecutingAssembly(); - var fileVersionInfo = FileVersionInfo.GetVersionInfo(CurrentAssembly.Location); - - // initialize log - LogManager.Initialize("HandheldCompanion"); - LogManager.LogInformation("{0} ({1})", CurrentAssembly.GetName(), fileVersionInfo.FileVersion); - - using (var process = Process.GetCurrentProcess()) - { - // force high priority - SetPriorityClass(process.Handle, (int)PriorityClass.HIGH_PRIORITY_CLASS); - - Process[] processes = Process.GetProcessesByName(process.ProcessName); - if (processes.Length > 1) - { - using (Process prevProcess = processes[0]) - { - nint handle = prevProcess.MainWindowHandle; - if (ProcessUtils.IsIconic(handle)) - ProcessUtils.ShowWindow(handle, (int)ProcessUtils.ShowWindowCommands.Restored); - - // force close this process if we were able to bring previous process to foreground - // kill previous process otherwise (means it's stalled) - if (ProcessUtils.SetForegroundWindow(handle)) - process.Kill(); - else - prevProcess.Kill(); - - return; - } - } - } - - // define culture settings - var CurrentCulture = SettingsManager.GetString("CurrentCulture"); - var culture = CultureInfo.CurrentCulture; - - switch (CurrentCulture) - { - default: - culture = new CultureInfo("en-US"); - break; - case "fr-FR": - case "en-US": - case "zh-CN": - case "zh-Hant": - case "de-DE": - case "it-IT": - case "pt-BR": - case "es-ES": - case "ja-JP": - case "ru-RU": - culture = new CultureInfo(CurrentCulture); - break; - } - - Thread.CurrentThread.CurrentCulture = culture; - Thread.CurrentThread.CurrentUICulture = culture; - CultureInfo.DefaultThreadCurrentCulture = culture; - CultureInfo.DefaultThreadCurrentUICulture = culture; - - // handle exceptions nicely - var currentDomain = default(AppDomain); - currentDomain = AppDomain.CurrentDomain; - // Handler for unhandled exceptions. - currentDomain.UnhandledException += CurrentDomain_UnhandledException; - // Handler for exceptions in threads behind forms. - System.Windows.Forms.Application.ThreadException += Application_ThreadException; - - MainWindow = new MainWindow(fileVersionInfo, CurrentAssembly); - MainWindow.Show(); - } - - private void Application_ThreadException(object sender, ThreadExceptionEventArgs e) - { - var ex = default(Exception); - ex = (Exception)e.Exception; - if (ex.InnerException != null) - { - LogManager.LogCritical(ex.InnerException.Message + "\t" + ex.InnerException.StackTrace); - } - LogManager.LogCritical(ex.Message + "\t" + ex.StackTrace); - } - - private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) - { - var ex = default(Exception); - ex = (Exception)e.ExceptionObject; - if (ex.InnerException != null) - { - LogManager.LogCritical(ex.InnerException.Message + "\t" + ex.InnerException.StackTrace); - } - LogManager.LogCritical(ex.Message + "\t" + ex.StackTrace); - } +using HandheldCompanion.Managers; +using HandheldCompanion.Utils; +using HandheldCompanion.Views; +using System; +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using System.Threading; +using System.Windows; +using static HandheldCompanion.WinAPI; + +namespace HandheldCompanion; + +/// +/// Interaction logic for App.xaml +/// +public partial class App : Application +{ + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + InitializeComponent(); + } + + /// + /// Invoked when the application is launched normally by the end user. Other entry points + /// will be used such as when the application is launched to open a specific file. + /// + /// Details about the launch request and process. + protected override void OnStartup(StartupEventArgs args) + { + // get current assembly + var CurrentAssembly = Assembly.GetExecutingAssembly(); + var fileVersionInfo = FileVersionInfo.GetVersionInfo(CurrentAssembly.Location); + + // initialize log + LogManager.Initialize("HandheldCompanion"); + LogManager.LogInformation("{0} ({1})", CurrentAssembly.GetName(), fileVersionInfo.FileVersion); + + using (var process = Process.GetCurrentProcess()) + { + // force high priority + SetPriorityClass(process.Handle, (int)PriorityClass.HIGH_PRIORITY_CLASS); + + Process[] processes = Process.GetProcessesByName(process.ProcessName); + if (processes.Length > 1) + { + using (Process prevProcess = processes[0]) + { + nint handle = prevProcess.MainWindowHandle; + if (ProcessUtils.IsIconic(handle)) + ProcessUtils.ShowWindow(handle, (int)ProcessUtils.ShowWindowCommands.Restored); + + // force close this process if we were able to bring previous process to foreground + // kill previous process otherwise (means it's stalled) + if (ProcessUtils.SetForegroundWindow(handle)) + process.Kill(); + else + prevProcess.Kill(); + + return; + } + } + } + + // define culture settings + var CurrentCulture = SettingsManager.GetString("CurrentCulture"); + var culture = CultureInfo.CurrentCulture; + + switch (CurrentCulture) + { + default: + culture = new CultureInfo("en-US"); + break; + case "fr-FR": + case "en-US": + case "zh-CN": + case "zh-Hant": + case "de-DE": + case "it-IT": + case "pt-BR": + case "es-ES": + case "ja-JP": + case "ru-RU": + culture = new CultureInfo(CurrentCulture); + break; + } + + Thread.CurrentThread.CurrentCulture = culture; + Thread.CurrentThread.CurrentUICulture = culture; + CultureInfo.DefaultThreadCurrentCulture = culture; + CultureInfo.DefaultThreadCurrentUICulture = culture; + + // handle exceptions nicely + var currentDomain = default(AppDomain); + currentDomain = AppDomain.CurrentDomain; + // Handler for unhandled exceptions. + currentDomain.UnhandledException += CurrentDomain_UnhandledException; + // Handler for exceptions in threads behind forms. + System.Windows.Forms.Application.ThreadException += Application_ThreadException; + + MainWindow = new MainWindow(fileVersionInfo, CurrentAssembly); + MainWindow.Show(); + } + + private void Application_ThreadException(object sender, ThreadExceptionEventArgs e) + { + var ex = default(Exception); + ex = (Exception)e.Exception; + if (ex.InnerException != null) + { + LogManager.LogCritical(ex.InnerException.Message + "\t" + ex.InnerException.StackTrace); + } + LogManager.LogCritical(ex.Message + "\t" + ex.StackTrace); + } + + private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + var ex = default(Exception); + ex = (Exception)e.ExceptionObject; + if (ex.InnerException != null) + { + LogManager.LogCritical(ex.InnerException.Message + "\t" + ex.InnerException.StackTrace); + } + LogManager.LogCritical(ex.Message + "\t" + ex.StackTrace); + } } \ No newline at end of file diff --git a/HandheldCompanion/Controllers/DInputController.cs b/HandheldCompanion/Controllers/DInputController.cs index 0c83ce3c7..7fa2c3ac2 100644 --- a/HandheldCompanion/Controllers/DInputController.cs +++ b/HandheldCompanion/Controllers/DInputController.cs @@ -1,71 +1,71 @@ -using SharpDX.DirectInput; - -namespace HandheldCompanion.Controllers; - -public class DInputController : IController -{ - public Joystick joystick; - protected JoystickState State = new(); - - public DInputController() - { } - - public DInputController(Joystick joystick, PnPDetails details) - { - if (joystick is null) - return; - - this.joystick = joystick; - UserIndex = (byte)joystick.Properties.JoystickId; - - if (details is null) - return; - - Details = details; - Details.isHooked = true; - - // Set BufferSize in order to use buffered data. - joystick.Properties.BufferSize = 128; - - // UI - DrawUI(); - UpdateUI(); - } - - public override string ToString() - { - var baseName = base.ToString(); - if (!string.IsNullOrEmpty(baseName)) - return baseName; - if (!string.IsNullOrEmpty(joystick.Information.ProductName)) - return joystick.Information.ProductName; - return $"DInput Controller {UserIndex}"; - } - - public override void UpdateInputs(long ticks, float delta) - { - base.UpdateInputs(ticks, delta); - } - - public override bool IsConnected() - { - return (bool)!joystick?.IsDisposed; - } - - public override void Plug() - { - if (joystick is not null) - joystick.Acquire(); - - base.Plug(); - } - - public override void Unplug() - { - // Unacquire the joystick - if (joystick is not null) - joystick.Unacquire(); - - base.Unplug(); - } +using SharpDX.DirectInput; + +namespace HandheldCompanion.Controllers; + +public class DInputController : IController +{ + public Joystick joystick; + protected JoystickState State = new(); + + public DInputController() + { } + + public DInputController(Joystick joystick, PnPDetails details) + { + if (joystick is null) + return; + + this.joystick = joystick; + UserIndex = (byte)joystick.Properties.JoystickId; + + if (details is null) + return; + + Details = details; + Details.isHooked = true; + + // Set BufferSize in order to use buffered data. + joystick.Properties.BufferSize = 128; + + // UI + DrawUI(); + UpdateUI(); + } + + public override string ToString() + { + var baseName = base.ToString(); + if (!string.IsNullOrEmpty(baseName)) + return baseName; + if (!string.IsNullOrEmpty(joystick.Information.ProductName)) + return joystick.Information.ProductName; + return $"DInput Controller {UserIndex}"; + } + + public override void UpdateInputs(long ticks, float delta) + { + base.UpdateInputs(ticks, delta); + } + + public override bool IsConnected() + { + return (bool)!joystick?.IsDisposed; + } + + public override void Plug() + { + if (joystick is not null) + joystick.Acquire(); + + base.Plug(); + } + + public override void Unplug() + { + // Unacquire the joystick + if (joystick is not null) + joystick.Unacquire(); + + base.Unplug(); + } } \ No newline at end of file diff --git a/HandheldCompanion/Controllers/DS4Controller.cs b/HandheldCompanion/Controllers/DS4Controller.cs index cb307cc08..4ef3ea723 100644 --- a/HandheldCompanion/Controllers/DS4Controller.cs +++ b/HandheldCompanion/Controllers/DS4Controller.cs @@ -1,180 +1,180 @@ -using HandheldCompanion.Inputs; -using HandheldCompanion.Managers; -using HandheldCompanion.Utils; -using System.Windows.Media; -using static JSL; - -namespace HandheldCompanion.Controllers; - -public class DS4Controller : JSController -{ - public DS4Controller() - { } - - public DS4Controller(JOY_SETTINGS settings, PnPDetails details) : base(settings, details) - { - // UI - ColoredButtons.Add(ButtonFlags.B1, new SolidColorBrush(Color.FromArgb(255, 116, 139, 255))); - ColoredButtons.Add(ButtonFlags.B2, new SolidColorBrush(Color.FromArgb(255, 255, 73, 75))); - ColoredButtons.Add(ButtonFlags.B3, new SolidColorBrush(Color.FromArgb(255, 244, 149, 193))); - ColoredButtons.Add(ButtonFlags.B4, new SolidColorBrush(Color.FromArgb(255, 73, 191, 115))); - } - - protected override void InitializeInputOutput() - { - // Additional controller specific source buttons - SourceButtons.Add(ButtonFlags.LeftPadClick); - SourceButtons.Add(ButtonFlags.LeftPadTouch); - SourceButtons.Add(ButtonFlags.RightPadClick); - SourceButtons.Add(ButtonFlags.RightPadTouch); - - SourceAxis.Add(AxisLayoutFlags.LeftPad); - SourceAxis.Add(AxisLayoutFlags.RightPad); - SourceAxis.Add(AxisLayoutFlags.Gyroscope); - - TargetButtons.Add(ButtonFlags.LeftPadClick); - TargetButtons.Add(ButtonFlags.LeftPadTouch); - TargetButtons.Add(ButtonFlags.RightPadTouch); - - TargetAxis.Add(AxisLayoutFlags.LeftPad); - TargetAxis.Add(AxisLayoutFlags.RightPad); - } - - public override void UpdateInputs(long ticks, float delta) - { - // skip if controller isn't connected - if (!IsConnected()) - return; - - base.UpdateState(delta); - - // Left Pad - Inputs.ButtonState[ButtonFlags.LeftPadTouch] = JslGetTouchDown(UserIndex); - Inputs.ButtonState[ButtonFlags.LeftPadClick] = BitwiseUtils.HasByteSet(sTATE.buttons, ButtonMaskCapture); - - if (Inputs.ButtonState[ButtonFlags.LeftPadTouch]) - { - float joyShockX0 = JslGetTouchX(UserIndex); - float joyShockY0 = JslGetTouchY(UserIndex); - - Inputs.AxisState[AxisFlags.LeftPadX] = (short)InputUtils.MapRange(joyShockX0, 0.0f, 1.0f, short.MinValue, short.MaxValue); - Inputs.AxisState[AxisFlags.LeftPadY] = (short)InputUtils.MapRange(joyShockY0, 0.0f, 1.0f, short.MaxValue, short.MinValue); - } - else - { - Inputs.AxisState[AxisFlags.LeftPadX] = 0; - Inputs.AxisState[AxisFlags.LeftPadY] = 0; - } - - // Right Pad - Inputs.ButtonState[ButtonFlags.RightPadTouch] = JslGetTouchDown(UserIndex, true); - Inputs.ButtonState[ButtonFlags.RightPadClick] = Inputs.ButtonState[ButtonFlags.LeftPadClick] && Inputs.ButtonState[ButtonFlags.RightPadTouch]; - - if (Inputs.ButtonState[ButtonFlags.RightPadTouch]) - { - float joyShockX1 = JslGetTouchX(UserIndex, true); - float joyShockY1 = JslGetTouchY(UserIndex, true); - - Inputs.AxisState[AxisFlags.RightPadX] = (short)InputUtils.MapRange(joyShockX1, 0.0f, 1.0f, short.MinValue, short.MaxValue); - Inputs.AxisState[AxisFlags.RightPadY] = (short)InputUtils.MapRange(joyShockY1, 0.0f, 1.0f, short.MaxValue, short.MinValue); - } - else - { - Inputs.AxisState[AxisFlags.RightPadX] = 0; - Inputs.AxisState[AxisFlags.RightPadY] = 0; - } - - base.UpdateInputs(ticks, delta); - } - - public override string ToString() - { - return "DUALSHOCK®4 Wireless Controller"; - } - - public override void Plug() - { - TimerManager.Tick += UpdateInputs; - base.Plug(); - } - - public override void Unplug() - { - TimerManager.Tick -= UpdateInputs; - base.Unplug(); - } - - public override void SetLightColor(byte R, byte G, byte B) - { - JslSetLightColour(UserIndex, CommonUtils.rgb_to_int(R, G, B)); - } - - public override string GetGlyph(ButtonFlags button) - { - switch (button) - { - case ButtonFlags.B1: - return "\u21E3"; // Cross - case ButtonFlags.B2: - return "\u21E2"; // Circle - case ButtonFlags.B3: - return "\u21E0"; // Square - case ButtonFlags.B4: - return "\u21E1"; // Triangle - case ButtonFlags.L1: - return "\u21B0"; - case ButtonFlags.R1: - return "\u21B1"; - case ButtonFlags.Back: - return "\u21E6"; - case ButtonFlags.Start: - return "\u21E8"; - case ButtonFlags.L2Soft: - return "\u21B2"; - case ButtonFlags.L2Full: - return "\u21B2"; - case ButtonFlags.R2Soft: - return "\u21B3"; - case ButtonFlags.R2Full: - return "\u21B3"; - case ButtonFlags.Special: - return "\uE000"; - case ButtonFlags.LeftPadClick: - case ButtonFlags.RightPadClick: - case ButtonFlags.LeftPadTouch: - case ButtonFlags.RightPadTouch: - return "\u21E7"; - } - - return base.GetGlyph(button); - } - - public override string GetGlyph(AxisFlags axis) - { - switch (axis) - { - case AxisFlags.L2: - return "\u21B2"; - case AxisFlags.R2: - return "\u21B3"; - } - - return base.GetGlyph(axis); - } - - public override string GetGlyph(AxisLayoutFlags axis) - { - switch (axis) - { - case AxisLayoutFlags.L2: - return "\u21B2"; - case AxisLayoutFlags.R2: - return "\u21B3"; - case AxisLayoutFlags.LeftPad: - case AxisLayoutFlags.RightPad: - return "\u21E7"; - } - - return base.GetGlyph(axis); - } +using HandheldCompanion.Inputs; +using HandheldCompanion.Managers; +using HandheldCompanion.Utils; +using System.Windows.Media; +using static JSL; + +namespace HandheldCompanion.Controllers; + +public class DS4Controller : JSController +{ + public DS4Controller() + { } + + public DS4Controller(JOY_SETTINGS settings, PnPDetails details) : base(settings, details) + { + // UI + ColoredButtons.Add(ButtonFlags.B1, new SolidColorBrush(Color.FromArgb(255, 116, 139, 255))); + ColoredButtons.Add(ButtonFlags.B2, new SolidColorBrush(Color.FromArgb(255, 255, 73, 75))); + ColoredButtons.Add(ButtonFlags.B3, new SolidColorBrush(Color.FromArgb(255, 244, 149, 193))); + ColoredButtons.Add(ButtonFlags.B4, new SolidColorBrush(Color.FromArgb(255, 73, 191, 115))); + } + + protected override void InitializeInputOutput() + { + // Additional controller specific source buttons + SourceButtons.Add(ButtonFlags.LeftPadClick); + SourceButtons.Add(ButtonFlags.LeftPadTouch); + SourceButtons.Add(ButtonFlags.RightPadClick); + SourceButtons.Add(ButtonFlags.RightPadTouch); + + SourceAxis.Add(AxisLayoutFlags.LeftPad); + SourceAxis.Add(AxisLayoutFlags.RightPad); + SourceAxis.Add(AxisLayoutFlags.Gyroscope); + + TargetButtons.Add(ButtonFlags.LeftPadClick); + TargetButtons.Add(ButtonFlags.LeftPadTouch); + TargetButtons.Add(ButtonFlags.RightPadTouch); + + TargetAxis.Add(AxisLayoutFlags.LeftPad); + TargetAxis.Add(AxisLayoutFlags.RightPad); + } + + public override void UpdateInputs(long ticks, float delta) + { + // skip if controller isn't connected + if (!IsConnected()) + return; + + base.UpdateState(delta); + + // Left Pad + Inputs.ButtonState[ButtonFlags.LeftPadTouch] = JslGetTouchDown(UserIndex); + Inputs.ButtonState[ButtonFlags.LeftPadClick] = BitwiseUtils.HasByteSet(sTATE.buttons, ButtonMaskCapture); + + if (Inputs.ButtonState[ButtonFlags.LeftPadTouch]) + { + float joyShockX0 = JslGetTouchX(UserIndex); + float joyShockY0 = JslGetTouchY(UserIndex); + + Inputs.AxisState[AxisFlags.LeftPadX] = (short)InputUtils.MapRange(joyShockX0, 0.0f, 1.0f, short.MinValue, short.MaxValue); + Inputs.AxisState[AxisFlags.LeftPadY] = (short)InputUtils.MapRange(joyShockY0, 0.0f, 1.0f, short.MaxValue, short.MinValue); + } + else + { + Inputs.AxisState[AxisFlags.LeftPadX] = 0; + Inputs.AxisState[AxisFlags.LeftPadY] = 0; + } + + // Right Pad + Inputs.ButtonState[ButtonFlags.RightPadTouch] = JslGetTouchDown(UserIndex, true); + Inputs.ButtonState[ButtonFlags.RightPadClick] = Inputs.ButtonState[ButtonFlags.LeftPadClick] && Inputs.ButtonState[ButtonFlags.RightPadTouch]; + + if (Inputs.ButtonState[ButtonFlags.RightPadTouch]) + { + float joyShockX1 = JslGetTouchX(UserIndex, true); + float joyShockY1 = JslGetTouchY(UserIndex, true); + + Inputs.AxisState[AxisFlags.RightPadX] = (short)InputUtils.MapRange(joyShockX1, 0.0f, 1.0f, short.MinValue, short.MaxValue); + Inputs.AxisState[AxisFlags.RightPadY] = (short)InputUtils.MapRange(joyShockY1, 0.0f, 1.0f, short.MaxValue, short.MinValue); + } + else + { + Inputs.AxisState[AxisFlags.RightPadX] = 0; + Inputs.AxisState[AxisFlags.RightPadY] = 0; + } + + base.UpdateInputs(ticks, delta); + } + + public override string ToString() + { + return "DUALSHOCK®4 Wireless Controller"; + } + + public override void Plug() + { + TimerManager.Tick += UpdateInputs; + base.Plug(); + } + + public override void Unplug() + { + TimerManager.Tick -= UpdateInputs; + base.Unplug(); + } + + public override void SetLightColor(byte R, byte G, byte B) + { + JslSetLightColour(UserIndex, CommonUtils.rgb_to_int(R, G, B)); + } + + public override string GetGlyph(ButtonFlags button) + { + switch (button) + { + case ButtonFlags.B1: + return "\u21E3"; // Cross + case ButtonFlags.B2: + return "\u21E2"; // Circle + case ButtonFlags.B3: + return "\u21E0"; // Square + case ButtonFlags.B4: + return "\u21E1"; // Triangle + case ButtonFlags.L1: + return "\u21B0"; + case ButtonFlags.R1: + return "\u21B1"; + case ButtonFlags.Back: + return "\u21E6"; + case ButtonFlags.Start: + return "\u21E8"; + case ButtonFlags.L2Soft: + return "\u21B2"; + case ButtonFlags.L2Full: + return "\u21B2"; + case ButtonFlags.R2Soft: + return "\u21B3"; + case ButtonFlags.R2Full: + return "\u21B3"; + case ButtonFlags.Special: + return "\uE000"; + case ButtonFlags.LeftPadClick: + case ButtonFlags.RightPadClick: + case ButtonFlags.LeftPadTouch: + case ButtonFlags.RightPadTouch: + return "\u21E7"; + } + + return base.GetGlyph(button); + } + + public override string GetGlyph(AxisFlags axis) + { + switch (axis) + { + case AxisFlags.L2: + return "\u21B2"; + case AxisFlags.R2: + return "\u21B3"; + } + + return base.GetGlyph(axis); + } + + public override string GetGlyph(AxisLayoutFlags axis) + { + switch (axis) + { + case AxisLayoutFlags.L2: + return "\u21B2"; + case AxisLayoutFlags.R2: + return "\u21B3"; + case AxisLayoutFlags.LeftPad: + case AxisLayoutFlags.RightPad: + return "\u21E7"; + } + + return base.GetGlyph(axis); + } } \ No newline at end of file diff --git a/HandheldCompanion/Controllers/DualSenseController.cs b/HandheldCompanion/Controllers/DualSenseController.cs index 8599b4362..9c3dce616 100644 --- a/HandheldCompanion/Controllers/DualSenseController.cs +++ b/HandheldCompanion/Controllers/DualSenseController.cs @@ -1,180 +1,180 @@ -using HandheldCompanion.Inputs; -using HandheldCompanion.Managers; -using HandheldCompanion.Utils; -using System.Windows.Media; -using static JSL; - -namespace HandheldCompanion.Controllers; - -public class DualSenseController : JSController -{ - public DualSenseController() - { } - - public DualSenseController(JOY_SETTINGS settings, PnPDetails details) : base(settings, details) - { - // UI - ColoredButtons.Add(ButtonFlags.B1, new SolidColorBrush(Color.FromArgb(255, 116, 139, 255))); - ColoredButtons.Add(ButtonFlags.B2, new SolidColorBrush(Color.FromArgb(255, 255, 73, 75))); - ColoredButtons.Add(ButtonFlags.B3, new SolidColorBrush(Color.FromArgb(255, 244, 149, 193))); - ColoredButtons.Add(ButtonFlags.B4, new SolidColorBrush(Color.FromArgb(255, 73, 191, 115))); - } - - protected override void InitializeInputOutput() - { - // Additional controller specific source buttons - SourceButtons.Add(ButtonFlags.LeftPadClick); - SourceButtons.Add(ButtonFlags.LeftPadTouch); - SourceButtons.Add(ButtonFlags.RightPadClick); - SourceButtons.Add(ButtonFlags.RightPadTouch); - - SourceAxis.Add(AxisLayoutFlags.LeftPad); - SourceAxis.Add(AxisLayoutFlags.RightPad); - SourceAxis.Add(AxisLayoutFlags.Gyroscope); - - TargetButtons.Add(ButtonFlags.LeftPadClick); - TargetButtons.Add(ButtonFlags.LeftPadTouch); - TargetButtons.Add(ButtonFlags.RightPadTouch); - - TargetAxis.Add(AxisLayoutFlags.LeftPad); - TargetAxis.Add(AxisLayoutFlags.RightPad); - } - - public override void UpdateInputs(long ticks, float delta) - { - // skip if controller isn't connected - if (!IsConnected()) - return; - - base.UpdateState(delta); - - // Left Pad - Inputs.ButtonState[ButtonFlags.LeftPadTouch] = JslGetTouchDown(UserIndex); - Inputs.ButtonState[ButtonFlags.LeftPadClick] = BitwiseUtils.HasByteSet(sTATE.buttons, ButtonMaskCapture); - - if (Inputs.ButtonState[ButtonFlags.LeftPadTouch]) - { - float joyShockX0 = JslGetTouchX(UserIndex); - float joyShockY0 = JslGetTouchY(UserIndex); - - Inputs.AxisState[AxisFlags.LeftPadX] = (short)InputUtils.MapRange(joyShockX0, 0.0f, 1.0f, short.MinValue, short.MaxValue); - Inputs.AxisState[AxisFlags.LeftPadY] = (short)InputUtils.MapRange(joyShockY0, 0.0f, 1.0f, short.MaxValue, short.MinValue); - } - else - { - Inputs.AxisState[AxisFlags.LeftPadX] = 0; - Inputs.AxisState[AxisFlags.LeftPadY] = 0; - } - - // Right Pad - Inputs.ButtonState[ButtonFlags.RightPadTouch] = JslGetTouchDown(UserIndex, true); - Inputs.ButtonState[ButtonFlags.RightPadClick] = Inputs.ButtonState[ButtonFlags.LeftPadClick] && Inputs.ButtonState[ButtonFlags.RightPadTouch]; - - if (Inputs.ButtonState[ButtonFlags.RightPadTouch]) - { - float joyShockX1 = JslGetTouchX(UserIndex, true); - float joyShockY1 = JslGetTouchY(UserIndex, true); - - Inputs.AxisState[AxisFlags.RightPadX] = (short)InputUtils.MapRange(joyShockX1, 0.0f, 1.0f, short.MinValue, short.MaxValue); - Inputs.AxisState[AxisFlags.RightPadY] = (short)InputUtils.MapRange(joyShockY1, 0.0f, 1.0f, short.MaxValue, short.MinValue); - } - else - { - Inputs.AxisState[AxisFlags.RightPadX] = 0; - Inputs.AxisState[AxisFlags.RightPadY] = 0; - } - - base.UpdateInputs(ticks, delta); - } - - public override string ToString() - { - return "DualSense® Wireless Controller"; - } - - public override void Plug() - { - TimerManager.Tick += UpdateInputs; - base.Plug(); - } - - public override void Unplug() - { - TimerManager.Tick -= UpdateInputs; - base.Unplug(); - } - - public override void SetLightColor(byte R, byte G, byte B) - { - JslSetLightColour(UserIndex, CommonUtils.rgb_to_int(R, G, B)); - } - - public override string GetGlyph(ButtonFlags button) - { - switch (button) - { - case ButtonFlags.B1: - return "\u21E3"; // Cross - case ButtonFlags.B2: - return "\u21E2"; // Circle - case ButtonFlags.B3: - return "\u21E0"; // Square - case ButtonFlags.B4: - return "\u21E1"; // Triangle - case ButtonFlags.L1: - return "\u21B0"; - case ButtonFlags.R1: - return "\u21B1"; - case ButtonFlags.Back: - return "\u2206"; - case ButtonFlags.Start: - return "\u2208"; - case ButtonFlags.L2Soft: - return "\u21B2"; - case ButtonFlags.L2Full: - return "\u21B2"; - case ButtonFlags.R2Soft: - return "\u21B3"; - case ButtonFlags.R2Full: - return "\u21B3"; - case ButtonFlags.Special: - return "\uE000"; - case ButtonFlags.LeftPadClick: - case ButtonFlags.RightPadClick: - case ButtonFlags.LeftPadTouch: - case ButtonFlags.RightPadTouch: - return "\u2207"; - } - - return base.GetGlyph(button); - } - - public override string GetGlyph(AxisFlags axis) - { - switch (axis) - { - case AxisFlags.L2: - return "\u21B2"; - case AxisFlags.R2: - return "\u21B3"; - } - - return base.GetGlyph(axis); - } - - public override string GetGlyph(AxisLayoutFlags axis) - { - switch (axis) - { - case AxisLayoutFlags.L2: - return "\u21B2"; - case AxisLayoutFlags.R2: - return "\u21B3"; - case AxisLayoutFlags.LeftPad: - case AxisLayoutFlags.RightPad: - return "\u2207"; - } - - return base.GetGlyph(axis); - } +using HandheldCompanion.Inputs; +using HandheldCompanion.Managers; +using HandheldCompanion.Utils; +using System.Windows.Media; +using static JSL; + +namespace HandheldCompanion.Controllers; + +public class DualSenseController : JSController +{ + public DualSenseController() + { } + + public DualSenseController(JOY_SETTINGS settings, PnPDetails details) : base(settings, details) + { + // UI + ColoredButtons.Add(ButtonFlags.B1, new SolidColorBrush(Color.FromArgb(255, 116, 139, 255))); + ColoredButtons.Add(ButtonFlags.B2, new SolidColorBrush(Color.FromArgb(255, 255, 73, 75))); + ColoredButtons.Add(ButtonFlags.B3, new SolidColorBrush(Color.FromArgb(255, 244, 149, 193))); + ColoredButtons.Add(ButtonFlags.B4, new SolidColorBrush(Color.FromArgb(255, 73, 191, 115))); + } + + protected override void InitializeInputOutput() + { + // Additional controller specific source buttons + SourceButtons.Add(ButtonFlags.LeftPadClick); + SourceButtons.Add(ButtonFlags.LeftPadTouch); + SourceButtons.Add(ButtonFlags.RightPadClick); + SourceButtons.Add(ButtonFlags.RightPadTouch); + + SourceAxis.Add(AxisLayoutFlags.LeftPad); + SourceAxis.Add(AxisLayoutFlags.RightPad); + SourceAxis.Add(AxisLayoutFlags.Gyroscope); + + TargetButtons.Add(ButtonFlags.LeftPadClick); + TargetButtons.Add(ButtonFlags.LeftPadTouch); + TargetButtons.Add(ButtonFlags.RightPadTouch); + + TargetAxis.Add(AxisLayoutFlags.LeftPad); + TargetAxis.Add(AxisLayoutFlags.RightPad); + } + + public override void UpdateInputs(long ticks, float delta) + { + // skip if controller isn't connected + if (!IsConnected()) + return; + + base.UpdateState(delta); + + // Left Pad + Inputs.ButtonState[ButtonFlags.LeftPadTouch] = JslGetTouchDown(UserIndex); + Inputs.ButtonState[ButtonFlags.LeftPadClick] = BitwiseUtils.HasByteSet(sTATE.buttons, ButtonMaskCapture); + + if (Inputs.ButtonState[ButtonFlags.LeftPadTouch]) + { + float joyShockX0 = JslGetTouchX(UserIndex); + float joyShockY0 = JslGetTouchY(UserIndex); + + Inputs.AxisState[AxisFlags.LeftPadX] = (short)InputUtils.MapRange(joyShockX0, 0.0f, 1.0f, short.MinValue, short.MaxValue); + Inputs.AxisState[AxisFlags.LeftPadY] = (short)InputUtils.MapRange(joyShockY0, 0.0f, 1.0f, short.MaxValue, short.MinValue); + } + else + { + Inputs.AxisState[AxisFlags.LeftPadX] = 0; + Inputs.AxisState[AxisFlags.LeftPadY] = 0; + } + + // Right Pad + Inputs.ButtonState[ButtonFlags.RightPadTouch] = JslGetTouchDown(UserIndex, true); + Inputs.ButtonState[ButtonFlags.RightPadClick] = Inputs.ButtonState[ButtonFlags.LeftPadClick] && Inputs.ButtonState[ButtonFlags.RightPadTouch]; + + if (Inputs.ButtonState[ButtonFlags.RightPadTouch]) + { + float joyShockX1 = JslGetTouchX(UserIndex, true); + float joyShockY1 = JslGetTouchY(UserIndex, true); + + Inputs.AxisState[AxisFlags.RightPadX] = (short)InputUtils.MapRange(joyShockX1, 0.0f, 1.0f, short.MinValue, short.MaxValue); + Inputs.AxisState[AxisFlags.RightPadY] = (short)InputUtils.MapRange(joyShockY1, 0.0f, 1.0f, short.MaxValue, short.MinValue); + } + else + { + Inputs.AxisState[AxisFlags.RightPadX] = 0; + Inputs.AxisState[AxisFlags.RightPadY] = 0; + } + + base.UpdateInputs(ticks, delta); + } + + public override string ToString() + { + return "DualSense® Wireless Controller"; + } + + public override void Plug() + { + TimerManager.Tick += UpdateInputs; + base.Plug(); + } + + public override void Unplug() + { + TimerManager.Tick -= UpdateInputs; + base.Unplug(); + } + + public override void SetLightColor(byte R, byte G, byte B) + { + JslSetLightColour(UserIndex, CommonUtils.rgb_to_int(R, G, B)); + } + + public override string GetGlyph(ButtonFlags button) + { + switch (button) + { + case ButtonFlags.B1: + return "\u21E3"; // Cross + case ButtonFlags.B2: + return "\u21E2"; // Circle + case ButtonFlags.B3: + return "\u21E0"; // Square + case ButtonFlags.B4: + return "\u21E1"; // Triangle + case ButtonFlags.L1: + return "\u21B0"; + case ButtonFlags.R1: + return "\u21B1"; + case ButtonFlags.Back: + return "\u2206"; + case ButtonFlags.Start: + return "\u2208"; + case ButtonFlags.L2Soft: + return "\u21B2"; + case ButtonFlags.L2Full: + return "\u21B2"; + case ButtonFlags.R2Soft: + return "\u21B3"; + case ButtonFlags.R2Full: + return "\u21B3"; + case ButtonFlags.Special: + return "\uE000"; + case ButtonFlags.LeftPadClick: + case ButtonFlags.RightPadClick: + case ButtonFlags.LeftPadTouch: + case ButtonFlags.RightPadTouch: + return "\u2207"; + } + + return base.GetGlyph(button); + } + + public override string GetGlyph(AxisFlags axis) + { + switch (axis) + { + case AxisFlags.L2: + return "\u21B2"; + case AxisFlags.R2: + return "\u21B3"; + } + + return base.GetGlyph(axis); + } + + public override string GetGlyph(AxisLayoutFlags axis) + { + switch (axis) + { + case AxisLayoutFlags.L2: + return "\u21B2"; + case AxisLayoutFlags.R2: + return "\u21B3"; + case AxisLayoutFlags.LeftPad: + case AxisLayoutFlags.RightPad: + return "\u2207"; + } + + return base.GetGlyph(axis); + } } \ No newline at end of file diff --git a/HandheldCompanion/Controllers/GordonController.cs b/HandheldCompanion/Controllers/GordonController.cs index 1d2ef137e..46185c1b6 100644 --- a/HandheldCompanion/Controllers/GordonController.cs +++ b/HandheldCompanion/Controllers/GordonController.cs @@ -1,322 +1,322 @@ -using HandheldCompanion.Actions; -using HandheldCompanion.Inputs; -using HandheldCompanion.Managers; -using HandheldCompanion.Utils; -using SharpDX.XInput; -using steam_hidapi.net; -using steam_hidapi.net.Hid; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using System.Windows.Media; - -namespace HandheldCompanion.Controllers -{ - public class GordonController : SteamController - { - private steam_hidapi.net.GordonController Controller; - private GordonControllerInputEventArgs input; - - private const short TrackPadInner = short.MaxValue / 2; - public const ushort MaxRumbleIntensity = 2048; - - public GordonController() - { } - - public GordonController(PnPDetails details) : base() - { - AttachDetails(details); - - // UI - ColoredButtons.Add(ButtonFlags.B1, new SolidColorBrush(Color.FromArgb(255, 81, 191, 61))); - ColoredButtons.Add(ButtonFlags.B2, new SolidColorBrush(Color.FromArgb(255, 217, 65, 38))); - ColoredButtons.Add(ButtonFlags.B3, new SolidColorBrush(Color.FromArgb(255, 26, 159, 255))); - ColoredButtons.Add(ButtonFlags.B4, new SolidColorBrush(Color.FromArgb(255, 255, 200, 44))); - - DrawUI(); - UpdateUI(); - } - - protected override void InitializeInputOutput() - { - // Additional controller specific source buttons/axes - SourceButtons.AddRange(new List() { ButtonFlags.L4, ButtonFlags.R4 }); - SourceButtons.AddRange(new List() { ButtonFlags.LeftPadClick, ButtonFlags.LeftPadTouch, ButtonFlags.LeftPadClickUp, ButtonFlags.LeftPadClickDown, ButtonFlags.LeftPadClickLeft, ButtonFlags.LeftPadClickRight }); - SourceButtons.AddRange(new List() { ButtonFlags.RightPadClick, ButtonFlags.RightPadTouch, ButtonFlags.RightPadClickUp, ButtonFlags.RightPadClickDown, ButtonFlags.RightPadClickLeft, ButtonFlags.RightPadClickRight }); - - SourceAxis.Add(AxisLayoutFlags.LeftPad); - SourceAxis.Add(AxisLayoutFlags.RightPad); - SourceAxis.Add(AxisLayoutFlags.Gyroscope); - - TargetButtons.Add(ButtonFlags.LeftPadClick); - TargetButtons.Add(ButtonFlags.RightPadClick); - TargetButtons.Add(ButtonFlags.LeftPadTouch); - TargetButtons.Add(ButtonFlags.RightPadTouch); - - TargetAxis.Add(AxisLayoutFlags.LeftPad); - TargetAxis.Add(AxisLayoutFlags.RightPad); - - // This is a very original controller, it doesn't have few things - SourceButtons.Remove(ButtonFlags.RightStickClick); - SourceButtons.Remove(ButtonFlags.RightStickUp); - SourceButtons.Remove(ButtonFlags.RightStickDown); - SourceButtons.Remove(ButtonFlags.RightStickLeft); - SourceButtons.Remove(ButtonFlags.RightStickRight); - - SourceAxis.Remove(AxisLayoutFlags.RightStick); - } - - public override void AttachDetails(PnPDetails details) - { - base.AttachDetails(details); - - Controller = new(details.VendorID, details.ProductID, details.GetMI()); - UserIndex = (byte)details.GetMI(); - - // open controller - Open(); - } - - public override string ToString() - { - string baseName = base.ToString(); - if (!string.IsNullOrEmpty(baseName)) - return baseName; - return "Steam Controller Gordon"; - } - - public override void UpdateInputs(long ticks, float delta) - { - if (input is null) - return; - - Inputs.ButtonState = InjectedButtons.Clone() as ButtonState; - - Inputs.ButtonState[ButtonFlags.B1] = input.State.ButtonState[GordonControllerButton.BtnA]; - Inputs.ButtonState[ButtonFlags.B2] = input.State.ButtonState[GordonControllerButton.BtnB]; - Inputs.ButtonState[ButtonFlags.B3] = input.State.ButtonState[GordonControllerButton.BtnX]; - Inputs.ButtonState[ButtonFlags.B4] = input.State.ButtonState[GordonControllerButton.BtnY]; - - Inputs.ButtonState[ButtonFlags.DPadUp] = input.State.ButtonState[GordonControllerButton.BtnDpadUp]; - Inputs.ButtonState[ButtonFlags.DPadDown] = input.State.ButtonState[GordonControllerButton.BtnDpadDown]; - Inputs.ButtonState[ButtonFlags.DPadLeft] = input.State.ButtonState[GordonControllerButton.BtnDpadLeft]; - Inputs.ButtonState[ButtonFlags.DPadRight] = input.State.ButtonState[GordonControllerButton.BtnDpadRight]; - - Inputs.ButtonState[ButtonFlags.Start] = input.State.ButtonState[GordonControllerButton.BtnOptions]; - Inputs.ButtonState[ButtonFlags.Back] = input.State.ButtonState[GordonControllerButton.BtnMenu]; - Inputs.ButtonState[ButtonFlags.Special] = input.State.ButtonState[GordonControllerButton.BtnSteam]; - - var L2 = input.State.AxesState[GordonControllerAxis.L2]; - var R2 = input.State.AxesState[GordonControllerAxis.R2]; - - Inputs.ButtonState[ButtonFlags.L2Soft] = L2 > Gamepad.TriggerThreshold; - Inputs.ButtonState[ButtonFlags.R2Soft] = R2 > Gamepad.TriggerThreshold; - - Inputs.ButtonState[ButtonFlags.L2Full] = L2 > Gamepad.TriggerThreshold * 8; - Inputs.ButtonState[ButtonFlags.R2Full] = R2 > Gamepad.TriggerThreshold * 8; - - Inputs.AxisState[AxisFlags.L2] = (short)L2; - Inputs.AxisState[AxisFlags.R2] = (short)R2; - - Inputs.ButtonState[ButtonFlags.L1] = input.State.ButtonState[GordonControllerButton.BtnL1]; - Inputs.ButtonState[ButtonFlags.R1] = input.State.ButtonState[GordonControllerButton.BtnR1]; - Inputs.ButtonState[ButtonFlags.L4] = input.State.ButtonState[GordonControllerButton.BtnL4]; - Inputs.ButtonState[ButtonFlags.R4] = input.State.ButtonState[GordonControllerButton.BtnR4]; - - // Left Stick - Inputs.ButtonState[ButtonFlags.LeftStickClick] = input.State.ButtonState[GordonControllerButton.BtnLStickPress]; - - Inputs.AxisState[AxisFlags.LeftStickX] = input.State.AxesState[GordonControllerAxis.LeftStickX]; - Inputs.AxisState[AxisFlags.LeftStickY] = input.State.AxesState[GordonControllerAxis.LeftStickY]; - - Inputs.ButtonState[ButtonFlags.LeftStickLeft] = Inputs.AxisState[AxisFlags.LeftStickX] < -Gamepad.LeftThumbDeadZone; - Inputs.ButtonState[ButtonFlags.LeftStickRight] = Inputs.AxisState[AxisFlags.LeftStickX] > Gamepad.LeftThumbDeadZone; - Inputs.ButtonState[ButtonFlags.LeftStickDown] = Inputs.AxisState[AxisFlags.LeftStickY] < -Gamepad.LeftThumbDeadZone; - Inputs.ButtonState[ButtonFlags.LeftStickUp] = Inputs.AxisState[AxisFlags.LeftStickY] > Gamepad.LeftThumbDeadZone; - - // TODO: Implement Inner/Outer Ring button mappings for sticks - // https://github.com/Havner/HandheldCompanion/commit/e1124ceb6c59051201756d5e95b2eb39a3bb24f6 - - /* float leftLength = new Vector2(Inputs.AxisState[AxisFlags.LeftThumbX], Inputs.AxisState[AxisFlags.LeftThumbY]).Length(); - Inputs.ButtonState[ButtonFlags.LeftStickOuterRing] = leftLength >= (RingThreshold * short.MaxValue); - Inputs.ButtonState[ButtonFlags.LeftStickInnerRing] = leftLength >= Gamepad.LeftThumbDeadZone && leftLength < (RingThreshold * short.MaxValue); */ - - // Left Pad - Inputs.ButtonState[ButtonFlags.LeftPadTouch] = input.State.ButtonState[GordonControllerButton.BtnLPadTouch]; - Inputs.ButtonState[ButtonFlags.LeftPadClick] = input.State.ButtonState[GordonControllerButton.BtnLPadPress]; - - if (Inputs.ButtonState[ButtonFlags.LeftPadTouch]) - { - Inputs.AxisState[AxisFlags.LeftPadX] = input.State.AxesState[GordonControllerAxis.LeftPadX]; - Inputs.AxisState[AxisFlags.LeftPadY] = input.State.AxesState[GordonControllerAxis.LeftPadY]; - } - else - { - Inputs.AxisState[AxisFlags.LeftPadX] = 0; - Inputs.AxisState[AxisFlags.LeftPadY] = 0; - } - - if (Inputs.ButtonState[ButtonFlags.LeftPadClick]) - { - InputUtils.TouchToDirections(Inputs.AxisState[AxisFlags.LeftPadX], Inputs.AxisState[AxisFlags.LeftPadY], TrackPadInner, 0, out bool[] buttons); - Inputs.ButtonState[ButtonFlags.LeftPadClickUp] = buttons[0]; - Inputs.ButtonState[ButtonFlags.LeftPadClickRight] = buttons[1]; - Inputs.ButtonState[ButtonFlags.LeftPadClickDown] = buttons[2]; - Inputs.ButtonState[ButtonFlags.LeftPadClickLeft] = buttons[3]; - } - else - { - Inputs.ButtonState[ButtonFlags.LeftPadClickUp] = false; - Inputs.ButtonState[ButtonFlags.LeftPadClickRight] = false; - Inputs.ButtonState[ButtonFlags.LeftPadClickDown] = false; - Inputs.ButtonState[ButtonFlags.LeftPadClickLeft] = false; - } - - // Right Pad - Inputs.ButtonState[ButtonFlags.RightPadTouch] = input.State.ButtonState[GordonControllerButton.BtnRPadTouch]; - Inputs.ButtonState[ButtonFlags.RightPadClick] = input.State.ButtonState[GordonControllerButton.BtnRPadPress]; - - if (Inputs.ButtonState[ButtonFlags.RightPadTouch]) - { - Inputs.AxisState[AxisFlags.RightPadX] = input.State.AxesState[GordonControllerAxis.RightPadX]; - Inputs.AxisState[AxisFlags.RightPadY] = input.State.AxesState[GordonControllerAxis.RightPadY]; - } - else - { - Inputs.AxisState[AxisFlags.RightPadX] = 0; - Inputs.AxisState[AxisFlags.RightPadY] = 0; - } - - if (Inputs.ButtonState[ButtonFlags.RightPadClick]) - { - InputUtils.TouchToDirections(Inputs.AxisState[AxisFlags.RightPadX], Inputs.AxisState[AxisFlags.RightPadY], TrackPadInner, 0, out bool[] buttons); - Inputs.ButtonState[ButtonFlags.RightPadClickUp] = buttons[0]; - Inputs.ButtonState[ButtonFlags.RightPadClickRight] = buttons[1]; - Inputs.ButtonState[ButtonFlags.RightPadClickDown] = buttons[2]; - Inputs.ButtonState[ButtonFlags.RightPadClickLeft] = buttons[3]; - } - else - { - Inputs.ButtonState[ButtonFlags.RightPadClickUp] = false; - Inputs.ButtonState[ButtonFlags.RightPadClickRight] = false; - Inputs.ButtonState[ButtonFlags.RightPadClickDown] = false; - Inputs.ButtonState[ButtonFlags.RightPadClickLeft] = false; - } - - // TODO: why Z/Y swapped? - float aX = (float)input.State.AxesState[GordonControllerAxis.GyroAccelX] / short.MaxValue * 2.0f; - float aY = (float)input.State.AxesState[GordonControllerAxis.GyroAccelZ] / short.MaxValue * 2.0f; - float aZ = -(float)input.State.AxesState[GordonControllerAxis.GyroAccelY] / short.MaxValue * 2.0f; - - // TODO: why Roll/Pitch swapped? - float gX = (float)input.State.AxesState[GordonControllerAxis.GyroPitch] / short.MaxValue * 2000.0f; // Roll - float gY = (float)input.State.AxesState[GordonControllerAxis.GyroRoll] / short.MaxValue * 2000.0f; // Pitch - float gZ = (float)input.State.AxesState[GordonControllerAxis.GyroYaw] / short.MaxValue * 2000.0f; // Yaw - - // store motion - Inputs.GyroState.SetGyroscope(gX, gY, gZ); - Inputs.GyroState.SetAccelerometer(aX, aY, aZ); - - // process motion - gamepadMotion.ProcessMotion(gX, gY, gZ, aX, aY, aZ, delta); - - base.UpdateInputs(ticks, delta); - } - private void OnControllerInputReceived(GordonControllerInputEventArgs input) - { - this.input = input; - } - - public override void Plug() - { - try - { - Controller.OnControllerInputReceived = input => Task.Run(() => OnControllerInputReceived(input)); - - // open controller - Open(); - } - catch (Exception ex) - { - LogManager.LogError("Couldn't initialize GordonController. Exception: {0}", ex.Message); - return; - } - - Controller.SetLizardMode(false); - Controller.SetGyroscope(true); - Controller.SetIdleTimeout(300); // ~5 min - - TimerManager.Tick += UpdateInputs; - - base.Plug(); - } - - public override void Unplug() - { - try - { - // restore lizard state - Controller.SetLizardMode(true); - Controller.SetGyroscope(false); - Controller.SetIdleTimeout(0); - //Controller.TurnOff(); // TODO: why not? - - // close controller - Close(); - } - catch - { - return; - } - - TimerManager.Tick -= UpdateInputs; - base.Unplug(); - } - - private void Open() - { - try - { - Controller.Open(); - isConnected = true; - } - catch { } - } - - private void Close() - { - try - { - Controller.Close(); - isConnected = false; - } - catch { } - } - - public ushort GetHapticIntensity(byte input, ushort maxIntensity) - { - return (ushort)(input * maxIntensity * VibrationStrength / 255); - } - - public override void SetVibration(byte LargeMotor, byte SmallMotor) - { - ushort leftAmplitude = GetHapticIntensity(LargeMotor, MaxRumbleIntensity); - Controller.SetHaptic((byte)SCHapticMotor.Left, leftAmplitude, 0, 1); - - ushort rightAmplitude = GetHapticIntensity(SmallMotor, MaxRumbleIntensity); - Controller.SetHaptic((byte)SCHapticMotor.Right, rightAmplitude, 0, 1); - } - - public override void SetHaptic(HapticStrength strength, ButtonFlags button) - { - ushort value = strength switch - { - HapticStrength.Low => 512, - HapticStrength.Medium => 1024, - HapticStrength.High => 2048, - _ => 0, - }; - Controller.SetHaptic((byte)GetMotorForButton(button), value, 0, 1); - } - } +using HandheldCompanion.Actions; +using HandheldCompanion.Inputs; +using HandheldCompanion.Managers; +using HandheldCompanion.Utils; +using SharpDX.XInput; +using steam_hidapi.net; +using steam_hidapi.net.Hid; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows.Media; + +namespace HandheldCompanion.Controllers +{ + public class GordonController : SteamController + { + private steam_hidapi.net.GordonController Controller; + private GordonControllerInputEventArgs input; + + private const short TrackPadInner = short.MaxValue / 2; + public const ushort MaxRumbleIntensity = 2048; + + public GordonController() + { } + + public GordonController(PnPDetails details) : base() + { + AttachDetails(details); + + // UI + ColoredButtons.Add(ButtonFlags.B1, new SolidColorBrush(Color.FromArgb(255, 81, 191, 61))); + ColoredButtons.Add(ButtonFlags.B2, new SolidColorBrush(Color.FromArgb(255, 217, 65, 38))); + ColoredButtons.Add(ButtonFlags.B3, new SolidColorBrush(Color.FromArgb(255, 26, 159, 255))); + ColoredButtons.Add(ButtonFlags.B4, new SolidColorBrush(Color.FromArgb(255, 255, 200, 44))); + + DrawUI(); + UpdateUI(); + } + + protected override void InitializeInputOutput() + { + // Additional controller specific source buttons/axes + SourceButtons.AddRange(new List() { ButtonFlags.L4, ButtonFlags.R4 }); + SourceButtons.AddRange(new List() { ButtonFlags.LeftPadClick, ButtonFlags.LeftPadTouch, ButtonFlags.LeftPadClickUp, ButtonFlags.LeftPadClickDown, ButtonFlags.LeftPadClickLeft, ButtonFlags.LeftPadClickRight }); + SourceButtons.AddRange(new List() { ButtonFlags.RightPadClick, ButtonFlags.RightPadTouch, ButtonFlags.RightPadClickUp, ButtonFlags.RightPadClickDown, ButtonFlags.RightPadClickLeft, ButtonFlags.RightPadClickRight }); + + SourceAxis.Add(AxisLayoutFlags.LeftPad); + SourceAxis.Add(AxisLayoutFlags.RightPad); + SourceAxis.Add(AxisLayoutFlags.Gyroscope); + + TargetButtons.Add(ButtonFlags.LeftPadClick); + TargetButtons.Add(ButtonFlags.RightPadClick); + TargetButtons.Add(ButtonFlags.LeftPadTouch); + TargetButtons.Add(ButtonFlags.RightPadTouch); + + TargetAxis.Add(AxisLayoutFlags.LeftPad); + TargetAxis.Add(AxisLayoutFlags.RightPad); + + // This is a very original controller, it doesn't have few things + SourceButtons.Remove(ButtonFlags.RightStickClick); + SourceButtons.Remove(ButtonFlags.RightStickUp); + SourceButtons.Remove(ButtonFlags.RightStickDown); + SourceButtons.Remove(ButtonFlags.RightStickLeft); + SourceButtons.Remove(ButtonFlags.RightStickRight); + + SourceAxis.Remove(AxisLayoutFlags.RightStick); + } + + public override void AttachDetails(PnPDetails details) + { + base.AttachDetails(details); + + Controller = new(details.VendorID, details.ProductID, details.GetMI()); + UserIndex = (byte)details.GetMI(); + + // open controller + Open(); + } + + public override string ToString() + { + string baseName = base.ToString(); + if (!string.IsNullOrEmpty(baseName)) + return baseName; + return "Steam Controller Gordon"; + } + + public override void UpdateInputs(long ticks, float delta) + { + if (input is null) + return; + + Inputs.ButtonState = InjectedButtons.Clone() as ButtonState; + + Inputs.ButtonState[ButtonFlags.B1] = input.State.ButtonState[GordonControllerButton.BtnA]; + Inputs.ButtonState[ButtonFlags.B2] = input.State.ButtonState[GordonControllerButton.BtnB]; + Inputs.ButtonState[ButtonFlags.B3] = input.State.ButtonState[GordonControllerButton.BtnX]; + Inputs.ButtonState[ButtonFlags.B4] = input.State.ButtonState[GordonControllerButton.BtnY]; + + Inputs.ButtonState[ButtonFlags.DPadUp] = input.State.ButtonState[GordonControllerButton.BtnDpadUp]; + Inputs.ButtonState[ButtonFlags.DPadDown] = input.State.ButtonState[GordonControllerButton.BtnDpadDown]; + Inputs.ButtonState[ButtonFlags.DPadLeft] = input.State.ButtonState[GordonControllerButton.BtnDpadLeft]; + Inputs.ButtonState[ButtonFlags.DPadRight] = input.State.ButtonState[GordonControllerButton.BtnDpadRight]; + + Inputs.ButtonState[ButtonFlags.Start] = input.State.ButtonState[GordonControllerButton.BtnOptions]; + Inputs.ButtonState[ButtonFlags.Back] = input.State.ButtonState[GordonControllerButton.BtnMenu]; + Inputs.ButtonState[ButtonFlags.Special] = input.State.ButtonState[GordonControllerButton.BtnSteam]; + + var L2 = input.State.AxesState[GordonControllerAxis.L2]; + var R2 = input.State.AxesState[GordonControllerAxis.R2]; + + Inputs.ButtonState[ButtonFlags.L2Soft] = L2 > Gamepad.TriggerThreshold; + Inputs.ButtonState[ButtonFlags.R2Soft] = R2 > Gamepad.TriggerThreshold; + + Inputs.ButtonState[ButtonFlags.L2Full] = L2 > Gamepad.TriggerThreshold * 8; + Inputs.ButtonState[ButtonFlags.R2Full] = R2 > Gamepad.TriggerThreshold * 8; + + Inputs.AxisState[AxisFlags.L2] = (short)L2; + Inputs.AxisState[AxisFlags.R2] = (short)R2; + + Inputs.ButtonState[ButtonFlags.L1] = input.State.ButtonState[GordonControllerButton.BtnL1]; + Inputs.ButtonState[ButtonFlags.R1] = input.State.ButtonState[GordonControllerButton.BtnR1]; + Inputs.ButtonState[ButtonFlags.L4] = input.State.ButtonState[GordonControllerButton.BtnL4]; + Inputs.ButtonState[ButtonFlags.R4] = input.State.ButtonState[GordonControllerButton.BtnR4]; + + // Left Stick + Inputs.ButtonState[ButtonFlags.LeftStickClick] = input.State.ButtonState[GordonControllerButton.BtnLStickPress]; + + Inputs.AxisState[AxisFlags.LeftStickX] = input.State.AxesState[GordonControllerAxis.LeftStickX]; + Inputs.AxisState[AxisFlags.LeftStickY] = input.State.AxesState[GordonControllerAxis.LeftStickY]; + + Inputs.ButtonState[ButtonFlags.LeftStickLeft] = Inputs.AxisState[AxisFlags.LeftStickX] < -Gamepad.LeftThumbDeadZone; + Inputs.ButtonState[ButtonFlags.LeftStickRight] = Inputs.AxisState[AxisFlags.LeftStickX] > Gamepad.LeftThumbDeadZone; + Inputs.ButtonState[ButtonFlags.LeftStickDown] = Inputs.AxisState[AxisFlags.LeftStickY] < -Gamepad.LeftThumbDeadZone; + Inputs.ButtonState[ButtonFlags.LeftStickUp] = Inputs.AxisState[AxisFlags.LeftStickY] > Gamepad.LeftThumbDeadZone; + + // TODO: Implement Inner/Outer Ring button mappings for sticks + // https://github.com/Havner/HandheldCompanion/commit/e1124ceb6c59051201756d5e95b2eb39a3bb24f6 + + /* float leftLength = new Vector2(Inputs.AxisState[AxisFlags.LeftThumbX], Inputs.AxisState[AxisFlags.LeftThumbY]).Length(); + Inputs.ButtonState[ButtonFlags.LeftStickOuterRing] = leftLength >= (RingThreshold * short.MaxValue); + Inputs.ButtonState[ButtonFlags.LeftStickInnerRing] = leftLength >= Gamepad.LeftThumbDeadZone && leftLength < (RingThreshold * short.MaxValue); */ + + // Left Pad + Inputs.ButtonState[ButtonFlags.LeftPadTouch] = input.State.ButtonState[GordonControllerButton.BtnLPadTouch]; + Inputs.ButtonState[ButtonFlags.LeftPadClick] = input.State.ButtonState[GordonControllerButton.BtnLPadPress]; + + if (Inputs.ButtonState[ButtonFlags.LeftPadTouch]) + { + Inputs.AxisState[AxisFlags.LeftPadX] = input.State.AxesState[GordonControllerAxis.LeftPadX]; + Inputs.AxisState[AxisFlags.LeftPadY] = input.State.AxesState[GordonControllerAxis.LeftPadY]; + } + else + { + Inputs.AxisState[AxisFlags.LeftPadX] = 0; + Inputs.AxisState[AxisFlags.LeftPadY] = 0; + } + + if (Inputs.ButtonState[ButtonFlags.LeftPadClick]) + { + InputUtils.TouchToDirections(Inputs.AxisState[AxisFlags.LeftPadX], Inputs.AxisState[AxisFlags.LeftPadY], TrackPadInner, 0, out bool[] buttons); + Inputs.ButtonState[ButtonFlags.LeftPadClickUp] = buttons[0]; + Inputs.ButtonState[ButtonFlags.LeftPadClickRight] = buttons[1]; + Inputs.ButtonState[ButtonFlags.LeftPadClickDown] = buttons[2]; + Inputs.ButtonState[ButtonFlags.LeftPadClickLeft] = buttons[3]; + } + else + { + Inputs.ButtonState[ButtonFlags.LeftPadClickUp] = false; + Inputs.ButtonState[ButtonFlags.LeftPadClickRight] = false; + Inputs.ButtonState[ButtonFlags.LeftPadClickDown] = false; + Inputs.ButtonState[ButtonFlags.LeftPadClickLeft] = false; + } + + // Right Pad + Inputs.ButtonState[ButtonFlags.RightPadTouch] = input.State.ButtonState[GordonControllerButton.BtnRPadTouch]; + Inputs.ButtonState[ButtonFlags.RightPadClick] = input.State.ButtonState[GordonControllerButton.BtnRPadPress]; + + if (Inputs.ButtonState[ButtonFlags.RightPadTouch]) + { + Inputs.AxisState[AxisFlags.RightPadX] = input.State.AxesState[GordonControllerAxis.RightPadX]; + Inputs.AxisState[AxisFlags.RightPadY] = input.State.AxesState[GordonControllerAxis.RightPadY]; + } + else + { + Inputs.AxisState[AxisFlags.RightPadX] = 0; + Inputs.AxisState[AxisFlags.RightPadY] = 0; + } + + if (Inputs.ButtonState[ButtonFlags.RightPadClick]) + { + InputUtils.TouchToDirections(Inputs.AxisState[AxisFlags.RightPadX], Inputs.AxisState[AxisFlags.RightPadY], TrackPadInner, 0, out bool[] buttons); + Inputs.ButtonState[ButtonFlags.RightPadClickUp] = buttons[0]; + Inputs.ButtonState[ButtonFlags.RightPadClickRight] = buttons[1]; + Inputs.ButtonState[ButtonFlags.RightPadClickDown] = buttons[2]; + Inputs.ButtonState[ButtonFlags.RightPadClickLeft] = buttons[3]; + } + else + { + Inputs.ButtonState[ButtonFlags.RightPadClickUp] = false; + Inputs.ButtonState[ButtonFlags.RightPadClickRight] = false; + Inputs.ButtonState[ButtonFlags.RightPadClickDown] = false; + Inputs.ButtonState[ButtonFlags.RightPadClickLeft] = false; + } + + // TODO: why Z/Y swapped? + float aX = (float)input.State.AxesState[GordonControllerAxis.GyroAccelX] / short.MaxValue * 2.0f; + float aY = (float)input.State.AxesState[GordonControllerAxis.GyroAccelZ] / short.MaxValue * 2.0f; + float aZ = -(float)input.State.AxesState[GordonControllerAxis.GyroAccelY] / short.MaxValue * 2.0f; + + // TODO: why Roll/Pitch swapped? + float gX = (float)input.State.AxesState[GordonControllerAxis.GyroPitch] / short.MaxValue * 2000.0f; // Roll + float gY = (float)input.State.AxesState[GordonControllerAxis.GyroRoll] / short.MaxValue * 2000.0f; // Pitch + float gZ = (float)input.State.AxesState[GordonControllerAxis.GyroYaw] / short.MaxValue * 2000.0f; // Yaw + + // store motion + Inputs.GyroState.SetGyroscope(gX, gY, gZ); + Inputs.GyroState.SetAccelerometer(aX, aY, aZ); + + // process motion + gamepadMotion.ProcessMotion(gX, gY, gZ, aX, aY, aZ, delta); + + base.UpdateInputs(ticks, delta); + } + private void OnControllerInputReceived(GordonControllerInputEventArgs input) + { + this.input = input; + } + + public override void Plug() + { + try + { + Controller.OnControllerInputReceived = input => Task.Run(() => OnControllerInputReceived(input)); + + // open controller + Open(); + } + catch (Exception ex) + { + LogManager.LogError("Couldn't initialize GordonController. Exception: {0}", ex.Message); + return; + } + + Controller.SetLizardMode(false); + Controller.SetGyroscope(true); + Controller.SetIdleTimeout(300); // ~5 min + + TimerManager.Tick += UpdateInputs; + + base.Plug(); + } + + public override void Unplug() + { + try + { + // restore lizard state + Controller.SetLizardMode(true); + Controller.SetGyroscope(false); + Controller.SetIdleTimeout(0); + //Controller.TurnOff(); // TODO: why not? + + // close controller + Close(); + } + catch + { + return; + } + + TimerManager.Tick -= UpdateInputs; + base.Unplug(); + } + + private void Open() + { + try + { + Controller.Open(); + isConnected = true; + } + catch { } + } + + private void Close() + { + try + { + Controller.Close(); + isConnected = false; + } + catch { } + } + + public ushort GetHapticIntensity(byte input, ushort maxIntensity) + { + return (ushort)(input * maxIntensity * VibrationStrength / 255); + } + + public override void SetVibration(byte LargeMotor, byte SmallMotor) + { + ushort leftAmplitude = GetHapticIntensity(LargeMotor, MaxRumbleIntensity); + Controller.SetHaptic((byte)SCHapticMotor.Left, leftAmplitude, 0, 1); + + ushort rightAmplitude = GetHapticIntensity(SmallMotor, MaxRumbleIntensity); + Controller.SetHaptic((byte)SCHapticMotor.Right, rightAmplitude, 0, 1); + } + + public override void SetHaptic(HapticStrength strength, ButtonFlags button) + { + ushort value = strength switch + { + HapticStrength.Low => 512, + HapticStrength.Medium => 1024, + HapticStrength.High => 2048, + _ => 0, + }; + Controller.SetHaptic((byte)GetMotorForButton(button), value, 0, 1); + } + } } \ No newline at end of file diff --git a/HandheldCompanion/Controllers/IController.xaml b/HandheldCompanion/Controllers/IController.xaml index 0ec1ea83d..ce6ea74f6 100644 --- a/HandheldCompanion/Controllers/IController.xaml +++ b/HandheldCompanion/Controllers/IController.xaml @@ -1,144 +1,144 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/HandheldCompanion/Views/Pages/LayoutPage.xaml.cs b/HandheldCompanion/Views/Pages/LayoutPage.xaml.cs index 31cf7eee9..f0bf8e113 100644 --- a/HandheldCompanion/Views/Pages/LayoutPage.xaml.cs +++ b/HandheldCompanion/Views/Pages/LayoutPage.xaml.cs @@ -1,522 +1,522 @@ -using HandheldCompanion.Controllers; -using HandheldCompanion.Controls; -using HandheldCompanion.Managers; -using HandheldCompanion.Misc; -using HandheldCompanion.Utils; -using iNKORE.UI.WPF.Modern.Controls; -using Nefarius.Utilities.DeviceManagement.PnP; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Navigation; -using Page = System.Windows.Controls.Page; - -namespace HandheldCompanion.Views.Pages; - - -public partial class LayoutPage : Page -{ - // Event to update ViewModel - public event UpdatedLayoutHandler LayoutUpdated; - public delegate void UpdatedLayoutHandler(Layout layout); - - // Getter to update layout in ViewModels - public Layout CurrentLayout => currentTemplate.Layout; - public LayoutTemplate currentTemplate = new(); - protected LockObject updateLock = new(); - - // page vars - private Dictionary pages; - private ButtonsPage buttonsPage; - private DpadPage dpadPage; - private GyroPage gyroPage; - private JoysticksPage joysticksPage; - private TrackpadsPage trackpadsPage; - private TriggersPage triggersPage; - - private NavigationView parentNavView; - private string preNavItemTag; - - public LayoutPage() - { - InitializeComponent(); - } - - public LayoutPage(string Tag, NavigationView parent) : this() - { - this.Tag = Tag; - this.parentNavView = parent; - } - - // Initialize pages later so the reference can be made to layoutPage from MainWindow - public void Initialize() - { - buttonsPage = new ButtonsPage(); - dpadPage = new DpadPage(); - gyroPage = new GyroPage(); - joysticksPage = new JoysticksPage(); - trackpadsPage = new TrackpadsPage(); - triggersPage = new TriggersPage(); - - // create controller related pages - this.pages = new() - { - // buttons - { "ButtonsPage", ( buttonsPage, navButtons ) }, - { "DpadPage", ( dpadPage, navDpad ) }, - - // triger - { "TriggersPage", ( triggersPage, navTriggers ) }, - - // axis - { "JoysticksPage", ( joysticksPage, navJoysticks ) }, - { "TrackpadsPage", ( trackpadsPage, navTrackpads ) }, - - // gyro - { "GyroPage", ( gyroPage, navGyro ) }, - }; - - // TODO: Temporary until conversion to MVVM - foreach (var template in LayoutManager.Templates) - { - LayoutManager_Updated(template); - } - - LayoutManager.Updated += LayoutManager_Updated; - LayoutManager.Initialized += LayoutManager_Initialized; - ControllerManager.ControllerSelected += ControllerManager_ControllerSelected; - SettingsManager.SettingValueChanged += SettingsManager_SettingValueChanged; - DeviceManager.UsbDeviceArrived += DeviceManager_UsbDeviceUpdated; - DeviceManager.UsbDeviceRemoved += DeviceManager_UsbDeviceUpdated; - ProfileManager.Updated += ProfileManager_Updated; - } - - private void ProfileManager_Updated(Profile profile, UpdateSource source, bool isCurrent) - { - if (!MainWindow.CurrentPageName.Equals("LayoutPage")) - return; - - Application.Current.Dispatcher.Invoke(() => - { - switch (source) - { - case UpdateSource.QuickProfilesPage: - { - if (currentTemplate.Name.Equals(profile.LayoutTitle)) - UpdateLayout(profile.Layout); - } - break; - } - }); - } - - private void ControllerManager_ControllerSelected(IController controller) - { - // UI thread (async) - Application.Current.Dispatcher.Invoke(() => - { - RefreshLayoutList(); - - // cascade update to (sub)pages - foreach (var page in pages.Values) - { - page.Item2.IsEnabled = page.Item1.IsEnabled(); - } - }); - } - - private void DeviceManager_UsbDeviceUpdated(PnPDevice device, DeviceEventArgs obj) - { - IController controller = ControllerManager.GetTargetController(); - - // lazy - if (controller is not null) - ControllerManager_ControllerSelected(controller); - } - - private void LayoutManager_Initialized() - { - // UI thread (async) - Application.Current.Dispatcher.Invoke(() => - { - RefreshLayoutList(); - }); - } - - private void LayoutManager_Updated(LayoutTemplate layoutTemplate) - { - // UI thread (async) - Application.Current.Dispatcher.Invoke(() => - { - // Get template separator index - var idx = -1; - - // search if we already have this template listed - foreach (var item in cB_Layouts.Items) - { - if (item is not ComboBoxItem) - continue; - - // get template - var parent = (ComboBoxItem)item; - if (parent.Content is not LayoutTemplate) - continue; - - var template = (LayoutTemplate)parent.Content; - if (template.Guid.Equals(layoutTemplate.Guid)) - { - idx = cB_Layouts.Items.IndexOf(parent); - break; - } - } - - if (idx != -1) - { - // found it - cB_Layouts.Items[idx] = new ComboBoxItem { Content = layoutTemplate }; - } - else - { - // new entry - if (layoutTemplate.IsInternal) - idx = cB_Layouts.Items.IndexOf(cB_LayoutsSplitterTemplates) + 1; - else - idx = cB_Layouts.Items.IndexOf(cB_LayoutsSplitterCommunity) + 1; - - cB_Layouts.Items.Insert(idx, new ComboBoxItem { Content = layoutTemplate }); - } - - cB_Layouts.Items.Refresh(); - cB_Layouts.InvalidateVisual(); - }); - } - - private void SettingsManager_SettingValueChanged(string? name, object value) - { - // UI thread - Application.Current.Dispatcher.Invoke(() => - { - switch (name) - { - case "LayoutFilterOnDevice": - CheckBoxDeviceLayouts.IsChecked = Convert.ToBoolean(value); - RefreshLayoutList(); - break; - } - }); - } - - private void RefreshLayoutList() - { - // Get filter settings - var FilterOnDevice = SettingsManager.GetBoolean("LayoutFilterOnDevice"); - - // Get current controller - var controller = ControllerManager.GetTargetController(); - - foreach (var layoutTemplate in LayoutManager.Templates) - { - // get parent - if (layoutTemplate.Parent is not ComboBoxItem parent) - continue; - - if (layoutTemplate.ControllerType is not null && FilterOnDevice) - if (layoutTemplate.ControllerType != controller?.GetType()) - { - parent.Visibility = Visibility.Collapsed; - continue; - } - - parent.Visibility = Visibility.Visible; - } - } - - private void Page_Loaded(object sender, RoutedEventArgs e) - { - } - - public void Page_Closed() - { - } - - private void Expander_Expanded(object sender, RoutedEventArgs e) - { - ((Expander)sender).BringIntoView(); - } - - public void UpdateLayout(Layout layout) - { - currentTemplate.Layout = layout; - - UpdatePages(); - } - - public void UpdateLayoutTemplate(LayoutTemplate layoutTemplate) - { - // TODO: Not entirely sure what is going on here, but the old templates were still sending - // events. Shouldn't they be destroyed? Either there is a bug or I don't understand something - // in C# (probably the latter). Either way this handles/fixes/workarounds the issue. - if (layoutTemplate.Layout != currentTemplate.Layout) - currentTemplate = layoutTemplate; - - UpdatePages(); - } - - private void UpdatePages() - { - // This is a very important lock, it blocks backward events to the layout when - // this is actually the backend that triggered the update. Notifications on higher - // levels (pages and mappings) could potentially be blocked for optimization. - using (new ScopedLock(updateLock)) - { - // UI thread (async) - Application.Current.Dispatcher.Invoke(() => - { - // Invoke Layout Updated to trigger ViewModel updates - LayoutUpdated?.Invoke(currentTemplate.Layout); - - // clear layout selection - cB_Layouts.SelectedValue = null; - - CheckBoxDefaultLayout.IsChecked = currentTemplate.Layout.IsDefaultLayout; - CheckBoxDefaultLayout.IsEnabled = currentTemplate.Layout != LayoutManager.GetDesktop(); - }); - } - } - - private void cB_Layouts_SizeChanged(object sender, SizeChangedEventArgs e) - { - var comboBox = (ComboBox)sender; - foreach (var item in comboBox.Items) - { - if (item is not ComboBoxItem) - continue; - - var layoutTemplate = (ComboBoxItem)item; - layoutTemplate.Width = comboBox.ActualWidth; - layoutTemplate.InvalidateVisual(); - } - } - - private async void ButtonApplyLayout_Click(object sender, RoutedEventArgs e) - { - if (cB_Layouts.SelectedItem is null) - return; - - if (cB_Layouts.SelectedItem is not ComboBoxItem) - return; - - // get parent - var parent = cB_Layouts.SelectedItem as ComboBoxItem; - - if (parent.Content is not LayoutTemplate) - return; - - Task dialogTask = new Dialog(MainWindow.GetCurrent()) - { - Title = string.Format(Properties.Resources.ProfilesPage_AreYouSureApplyTemplate1, currentTemplate.Name), - Content = string.Format(Properties.Resources.ProfilesPage_AreYouSureApplyTemplate2, currentTemplate.Name), - DefaultButton = ContentDialogButton.Close, - CloseButtonText = Properties.Resources.ProfilesPage_Cancel, - PrimaryButtonText = Properties.Resources.ProfilesPage_Yes - }.ShowAsync(); - - await dialogTask; // sync call - - switch (dialogTask.Result) - { - case ContentDialogResult.Primary: - { - // do not overwrite currentTemplate and currentTemplate.Layout as a whole - // because they both have important Update notifitications set - - // get template - LayoutTemplate layoutTemplate = (LayoutTemplate)parent.Content; - var newLayout = layoutTemplate.Layout.Clone() as Layout; - currentTemplate.Layout.AxisLayout = newLayout.AxisLayout; - currentTemplate.Layout.ButtonLayout = newLayout.ButtonLayout; - currentTemplate.Layout.GyroLayout = newLayout.GyroLayout; - - currentTemplate.Name = layoutTemplate.Name; - currentTemplate.Description = layoutTemplate.Description; - currentTemplate.Guid = layoutTemplate.Guid; // not needed - - // the whole layout has been updated without notification, trigger one - currentTemplate.Layout.UpdateLayout(); - - UpdatePages(); - } - break; - } - } - - private void cB_Layouts_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - ButtonApplyLayout.IsEnabled = cB_Layouts.SelectedIndex != -1; - } - - private void CheckBoxDeviceLayouts_Checked(object sender, RoutedEventArgs e) - { - if (!IsLoaded) - return; - - SettingsManager.SetProperty("LayoutFilterOnDevice", CheckBoxDeviceLayouts.IsChecked); - } - - private void LayoutExportButton_Click(object sender, RoutedEventArgs e) - { - LayoutTemplate newLayout = new() - { - Layout = currentTemplate.Layout, - Name = LayoutTitle.Text, - Description = LayoutDescription.Text, - Author = LayoutAuthor.Text, - Executable = SaveGameInfo.IsChecked == true ? currentTemplate.Executable : "", - Product = SaveGameInfo.IsChecked == true ? currentTemplate.Product : "", - IsInternal = false - }; - - if (newLayout.Name == string.Empty) - { - LayoutFlyout.Hide(); - - // todo: translate me - _ = new Dialog(MainWindow.GetCurrent()) - { - Title = "Layout template name cannot be empty", - Content = "Layout was not exported.", - PrimaryButtonText = Properties.Resources.ProfilesPage_OK - }.ShowAsync(); - - return; - } - - if (ExportForCurrent.IsChecked == true) - newLayout.ControllerType = ControllerManager.GetTargetController()?.GetType(); - - LayoutManager.SerializeLayoutTemplate(newLayout); - - // todo: translate me - _ = new Dialog(MainWindow.GetCurrent()) - { - Title = "Layout template exported", - Content = $"{newLayout.Name} was exported.", - PrimaryButtonText = Properties.Resources.ProfilesPage_OK - }.ShowAsync(); - } - - private void Flyout_Opening(object sender, object e) - { - if (currentTemplate.Executable == string.Empty && currentTemplate.Product == string.Empty) - SaveGameInfo.IsChecked = SaveGameInfo.IsEnabled = false; - else - SaveGameInfo.IsChecked = SaveGameInfo.IsEnabled = true; - - var separator = currentTemplate.Name.Length > 0 && currentTemplate.Product.Length > 0 ? " - " : ""; - - LayoutTitle.Text = $"{currentTemplate.Name}{separator}{currentTemplate.Product}"; - LayoutDescription.Text = currentTemplate.Description; - LayoutAuthor.Text = currentTemplate.Author; - } - - private void SaveGameInfo_Toggled(object sender, RoutedEventArgs e) - { - if (SaveGameInfo.IsChecked == true) - { - var separator = currentTemplate.Name.Length > 0 && currentTemplate.Product.Length > 0 ? " - " : ""; - LayoutTitle.Text = $"{currentTemplate.Name}{separator}{currentTemplate.Product}"; - } - else - { - LayoutTitle.Text = $"{currentTemplate.Name}"; - } - } - - private void LayoutCancelButton_Click(object sender, RoutedEventArgs e) - { - LayoutFlyout.Hide(); - } - - private void navView_ItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args) - { - if (args.InvokedItemContainer is null) - return; - - NavigationViewItem navItem = (NavigationViewItem)args.InvokedItemContainer; - preNavItemTag = (string)navItem.Tag; - NavView_Navigate(preNavItemTag); - } - - public void NavView_Navigate(string navItemTag) - { - var item = pages.FirstOrDefault(p => p.Key.Equals(navItemTag)); - Page _page = item.Value.Item1; - - // Get the page type before navigation so you can prevent duplicate - // entries in the backstack. - var preNavPageType = ContentFrame.CurrentSourcePageType; - - // Only navigate if the selected page isn't currently loaded. - if (!(_page is null) && !Equals(preNavPageType, _page)) NavView_Navigate(_page); - } - - public void NavView_Navigate(Page _page) - { - ContentFrame.Navigate(_page); - } - - private void navView_Loaded(object sender, RoutedEventArgs e) - { - // Add handler for ContentFrame navigation. - ContentFrame.Navigated += On_Navigated; - - // NavView doesn't load any page by default, so load home page. - navView.SelectedItem = navView.MenuItems[0]; - - // If navigation occurs on SelectionChanged, this isn't needed. - // Because we use ItemInvoked to navigate, we need to call Navigate - // here to load the home page. - preNavItemTag = "ButtonsPage"; - NavView_Navigate(preNavItemTag); - } - - private void On_Navigated(object sender, NavigationEventArgs e) - { - navView.IsBackEnabled = ContentFrame.CanGoBack; - - if (ContentFrame.SourcePageType is not null) - { - var preNavPageType = ContentFrame.CurrentSourcePageType; - var preNavPageName = preNavPageType.Name; - - var NavViewItem = navView.MenuItems - .OfType().FirstOrDefault(n => n.Tag.Equals(preNavPageName)); - - if (!(NavViewItem is null)) - navView.SelectedItem = NavViewItem; - - string header = currentTemplate.Product.Length > 0 ? - "Profile: " + currentTemplate.Product : "Layout: Desktop"; - parentNavView.Header = new TextBlock() { Text = header }; - } - } - - private void CheckBoxDefaultLayout_Checked(object sender, RoutedEventArgs e) - { - var isDefaultLayout = (bool)CheckBoxDefaultLayout.IsChecked; - var prevDefaultLayoutProfile = ProfileManager.GetProfileWithDefaultLayout(); - - currentTemplate.Layout.IsDefaultLayout = isDefaultLayout; - currentTemplate.Layout.UpdateLayout(); - - // If option is enabled and a different default layout profile exists, we want to set option false on prev profile. - if (isDefaultLayout && prevDefaultLayoutProfile != null && prevDefaultLayoutProfile.Layout != currentTemplate.Layout) - { - prevDefaultLayoutProfile.Layout.IsDefaultLayout = false; - ProfileManager.UpdateOrCreateProfile(prevDefaultLayoutProfile); - } - } +using HandheldCompanion.Controllers; +using HandheldCompanion.Controls; +using HandheldCompanion.Managers; +using HandheldCompanion.Misc; +using HandheldCompanion.Utils; +using iNKORE.UI.WPF.Modern.Controls; +using Nefarius.Utilities.DeviceManagement.PnP; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Navigation; +using Page = System.Windows.Controls.Page; + +namespace HandheldCompanion.Views.Pages; + + +public partial class LayoutPage : Page +{ + // Event to update ViewModel + public event UpdatedLayoutHandler LayoutUpdated; + public delegate void UpdatedLayoutHandler(Layout layout); + + // Getter to update layout in ViewModels + public Layout CurrentLayout => currentTemplate.Layout; + public LayoutTemplate currentTemplate = new(); + protected LockObject updateLock = new(); + + // page vars + private Dictionary pages; + private ButtonsPage buttonsPage; + private DpadPage dpadPage; + private GyroPage gyroPage; + private JoysticksPage joysticksPage; + private TrackpadsPage trackpadsPage; + private TriggersPage triggersPage; + + private NavigationView parentNavView; + private string preNavItemTag; + + public LayoutPage() + { + InitializeComponent(); + } + + public LayoutPage(string Tag, NavigationView parent) : this() + { + this.Tag = Tag; + this.parentNavView = parent; + } + + // Initialize pages later so the reference can be made to layoutPage from MainWindow + public void Initialize() + { + buttonsPage = new ButtonsPage(); + dpadPage = new DpadPage(); + gyroPage = new GyroPage(); + joysticksPage = new JoysticksPage(); + trackpadsPage = new TrackpadsPage(); + triggersPage = new TriggersPage(); + + // create controller related pages + this.pages = new() + { + // buttons + { "ButtonsPage", ( buttonsPage, navButtons ) }, + { "DpadPage", ( dpadPage, navDpad ) }, + + // triger + { "TriggersPage", ( triggersPage, navTriggers ) }, + + // axis + { "JoysticksPage", ( joysticksPage, navJoysticks ) }, + { "TrackpadsPage", ( trackpadsPage, navTrackpads ) }, + + // gyro + { "GyroPage", ( gyroPage, navGyro ) }, + }; + + // TODO: Temporary until conversion to MVVM + foreach (var template in LayoutManager.Templates) + { + LayoutManager_Updated(template); + } + + LayoutManager.Updated += LayoutManager_Updated; + LayoutManager.Initialized += LayoutManager_Initialized; + ControllerManager.ControllerSelected += ControllerManager_ControllerSelected; + SettingsManager.SettingValueChanged += SettingsManager_SettingValueChanged; + DeviceManager.UsbDeviceArrived += DeviceManager_UsbDeviceUpdated; + DeviceManager.UsbDeviceRemoved += DeviceManager_UsbDeviceUpdated; + ProfileManager.Updated += ProfileManager_Updated; + } + + private void ProfileManager_Updated(Profile profile, UpdateSource source, bool isCurrent) + { + if (!MainWindow.CurrentPageName.Equals("LayoutPage")) + return; + + Application.Current.Dispatcher.Invoke(() => + { + switch (source) + { + case UpdateSource.QuickProfilesPage: + { + if (currentTemplate.Name.Equals(profile.LayoutTitle)) + UpdateLayout(profile.Layout); + } + break; + } + }); + } + + private void ControllerManager_ControllerSelected(IController controller) + { + // UI thread (async) + Application.Current.Dispatcher.Invoke(() => + { + RefreshLayoutList(); + + // cascade update to (sub)pages + foreach (var page in pages.Values) + { + page.Item2.IsEnabled = page.Item1.IsEnabled(); + } + }); + } + + private void DeviceManager_UsbDeviceUpdated(PnPDevice device, DeviceEventArgs obj) + { + IController controller = ControllerManager.GetTargetController(); + + // lazy + if (controller is not null) + ControllerManager_ControllerSelected(controller); + } + + private void LayoutManager_Initialized() + { + // UI thread (async) + Application.Current.Dispatcher.Invoke(() => + { + RefreshLayoutList(); + }); + } + + private void LayoutManager_Updated(LayoutTemplate layoutTemplate) + { + // UI thread (async) + Application.Current.Dispatcher.Invoke(() => + { + // Get template separator index + var idx = -1; + + // search if we already have this template listed + foreach (var item in cB_Layouts.Items) + { + if (item is not ComboBoxItem) + continue; + + // get template + var parent = (ComboBoxItem)item; + if (parent.Content is not LayoutTemplate) + continue; + + var template = (LayoutTemplate)parent.Content; + if (template.Guid.Equals(layoutTemplate.Guid)) + { + idx = cB_Layouts.Items.IndexOf(parent); + break; + } + } + + if (idx != -1) + { + // found it + cB_Layouts.Items[idx] = new ComboBoxItem { Content = layoutTemplate }; + } + else + { + // new entry + if (layoutTemplate.IsInternal) + idx = cB_Layouts.Items.IndexOf(cB_LayoutsSplitterTemplates) + 1; + else + idx = cB_Layouts.Items.IndexOf(cB_LayoutsSplitterCommunity) + 1; + + cB_Layouts.Items.Insert(idx, new ComboBoxItem { Content = layoutTemplate }); + } + + cB_Layouts.Items.Refresh(); + cB_Layouts.InvalidateVisual(); + }); + } + + private void SettingsManager_SettingValueChanged(string? name, object value) + { + // UI thread + Application.Current.Dispatcher.Invoke(() => + { + switch (name) + { + case "LayoutFilterOnDevice": + CheckBoxDeviceLayouts.IsChecked = Convert.ToBoolean(value); + RefreshLayoutList(); + break; + } + }); + } + + private void RefreshLayoutList() + { + // Get filter settings + var FilterOnDevice = SettingsManager.GetBoolean("LayoutFilterOnDevice"); + + // Get current controller + var controller = ControllerManager.GetTargetController(); + + foreach (var layoutTemplate in LayoutManager.Templates) + { + // get parent + if (layoutTemplate.Parent is not ComboBoxItem parent) + continue; + + if (layoutTemplate.ControllerType is not null && FilterOnDevice) + if (layoutTemplate.ControllerType != controller?.GetType()) + { + parent.Visibility = Visibility.Collapsed; + continue; + } + + parent.Visibility = Visibility.Visible; + } + } + + private void Page_Loaded(object sender, RoutedEventArgs e) + { + } + + public void Page_Closed() + { + } + + private void Expander_Expanded(object sender, RoutedEventArgs e) + { + ((Expander)sender).BringIntoView(); + } + + public void UpdateLayout(Layout layout) + { + currentTemplate.Layout = layout; + + UpdatePages(); + } + + public void UpdateLayoutTemplate(LayoutTemplate layoutTemplate) + { + // TODO: Not entirely sure what is going on here, but the old templates were still sending + // events. Shouldn't they be destroyed? Either there is a bug or I don't understand something + // in C# (probably the latter). Either way this handles/fixes/workarounds the issue. + if (layoutTemplate.Layout != currentTemplate.Layout) + currentTemplate = layoutTemplate; + + UpdatePages(); + } + + private void UpdatePages() + { + // This is a very important lock, it blocks backward events to the layout when + // this is actually the backend that triggered the update. Notifications on higher + // levels (pages and mappings) could potentially be blocked for optimization. + using (new ScopedLock(updateLock)) + { + // UI thread (async) + Application.Current.Dispatcher.Invoke(() => + { + // Invoke Layout Updated to trigger ViewModel updates + LayoutUpdated?.Invoke(currentTemplate.Layout); + + // clear layout selection + cB_Layouts.SelectedValue = null; + + CheckBoxDefaultLayout.IsChecked = currentTemplate.Layout.IsDefaultLayout; + CheckBoxDefaultLayout.IsEnabled = currentTemplate.Layout != LayoutManager.GetDesktop(); + }); + } + } + + private void cB_Layouts_SizeChanged(object sender, SizeChangedEventArgs e) + { + var comboBox = (ComboBox)sender; + foreach (var item in comboBox.Items) + { + if (item is not ComboBoxItem) + continue; + + var layoutTemplate = (ComboBoxItem)item; + layoutTemplate.Width = comboBox.ActualWidth; + layoutTemplate.InvalidateVisual(); + } + } + + private async void ButtonApplyLayout_Click(object sender, RoutedEventArgs e) + { + if (cB_Layouts.SelectedItem is null) + return; + + if (cB_Layouts.SelectedItem is not ComboBoxItem) + return; + + // get parent + var parent = cB_Layouts.SelectedItem as ComboBoxItem; + + if (parent.Content is not LayoutTemplate) + return; + + Task dialogTask = new Dialog(MainWindow.GetCurrent()) + { + Title = string.Format(Properties.Resources.ProfilesPage_AreYouSureApplyTemplate1, currentTemplate.Name), + Content = string.Format(Properties.Resources.ProfilesPage_AreYouSureApplyTemplate2, currentTemplate.Name), + DefaultButton = ContentDialogButton.Close, + CloseButtonText = Properties.Resources.ProfilesPage_Cancel, + PrimaryButtonText = Properties.Resources.ProfilesPage_Yes + }.ShowAsync(); + + await dialogTask; // sync call + + switch (dialogTask.Result) + { + case ContentDialogResult.Primary: + { + // do not overwrite currentTemplate and currentTemplate.Layout as a whole + // because they both have important Update notifitications set + + // get template + LayoutTemplate layoutTemplate = (LayoutTemplate)parent.Content; + var newLayout = layoutTemplate.Layout.Clone() as Layout; + currentTemplate.Layout.AxisLayout = newLayout.AxisLayout; + currentTemplate.Layout.ButtonLayout = newLayout.ButtonLayout; + currentTemplate.Layout.GyroLayout = newLayout.GyroLayout; + + currentTemplate.Name = layoutTemplate.Name; + currentTemplate.Description = layoutTemplate.Description; + currentTemplate.Guid = layoutTemplate.Guid; // not needed + + // the whole layout has been updated without notification, trigger one + currentTemplate.Layout.UpdateLayout(); + + UpdatePages(); + } + break; + } + } + + private void cB_Layouts_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + ButtonApplyLayout.IsEnabled = cB_Layouts.SelectedIndex != -1; + } + + private void CheckBoxDeviceLayouts_Checked(object sender, RoutedEventArgs e) + { + if (!IsLoaded) + return; + + SettingsManager.SetProperty("LayoutFilterOnDevice", CheckBoxDeviceLayouts.IsChecked); + } + + private void LayoutExportButton_Click(object sender, RoutedEventArgs e) + { + LayoutTemplate newLayout = new() + { + Layout = currentTemplate.Layout, + Name = LayoutTitle.Text, + Description = LayoutDescription.Text, + Author = LayoutAuthor.Text, + Executable = SaveGameInfo.IsChecked == true ? currentTemplate.Executable : "", + Product = SaveGameInfo.IsChecked == true ? currentTemplate.Product : "", + IsInternal = false + }; + + if (newLayout.Name == string.Empty) + { + LayoutFlyout.Hide(); + + // todo: translate me + _ = new Dialog(MainWindow.GetCurrent()) + { + Title = "Layout template name cannot be empty", + Content = "Layout was not exported.", + PrimaryButtonText = Properties.Resources.ProfilesPage_OK + }.ShowAsync(); + + return; + } + + if (ExportForCurrent.IsChecked == true) + newLayout.ControllerType = ControllerManager.GetTargetController()?.GetType(); + + LayoutManager.SerializeLayoutTemplate(newLayout); + + // todo: translate me + _ = new Dialog(MainWindow.GetCurrent()) + { + Title = "Layout template exported", + Content = $"{newLayout.Name} was exported.", + PrimaryButtonText = Properties.Resources.ProfilesPage_OK + }.ShowAsync(); + } + + private void Flyout_Opening(object sender, object e) + { + if (currentTemplate.Executable == string.Empty && currentTemplate.Product == string.Empty) + SaveGameInfo.IsChecked = SaveGameInfo.IsEnabled = false; + else + SaveGameInfo.IsChecked = SaveGameInfo.IsEnabled = true; + + var separator = currentTemplate.Name.Length > 0 && currentTemplate.Product.Length > 0 ? " - " : ""; + + LayoutTitle.Text = $"{currentTemplate.Name}{separator}{currentTemplate.Product}"; + LayoutDescription.Text = currentTemplate.Description; + LayoutAuthor.Text = currentTemplate.Author; + } + + private void SaveGameInfo_Toggled(object sender, RoutedEventArgs e) + { + if (SaveGameInfo.IsChecked == true) + { + var separator = currentTemplate.Name.Length > 0 && currentTemplate.Product.Length > 0 ? " - " : ""; + LayoutTitle.Text = $"{currentTemplate.Name}{separator}{currentTemplate.Product}"; + } + else + { + LayoutTitle.Text = $"{currentTemplate.Name}"; + } + } + + private void LayoutCancelButton_Click(object sender, RoutedEventArgs e) + { + LayoutFlyout.Hide(); + } + + private void navView_ItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args) + { + if (args.InvokedItemContainer is null) + return; + + NavigationViewItem navItem = (NavigationViewItem)args.InvokedItemContainer; + preNavItemTag = (string)navItem.Tag; + NavView_Navigate(preNavItemTag); + } + + public void NavView_Navigate(string navItemTag) + { + var item = pages.FirstOrDefault(p => p.Key.Equals(navItemTag)); + Page _page = item.Value.Item1; + + // Get the page type before navigation so you can prevent duplicate + // entries in the backstack. + var preNavPageType = ContentFrame.CurrentSourcePageType; + + // Only navigate if the selected page isn't currently loaded. + if (!(_page is null) && !Equals(preNavPageType, _page)) NavView_Navigate(_page); + } + + public void NavView_Navigate(Page _page) + { + ContentFrame.Navigate(_page); + } + + private void navView_Loaded(object sender, RoutedEventArgs e) + { + // Add handler for ContentFrame navigation. + ContentFrame.Navigated += On_Navigated; + + // NavView doesn't load any page by default, so load home page. + navView.SelectedItem = navView.MenuItems[0]; + + // If navigation occurs on SelectionChanged, this isn't needed. + // Because we use ItemInvoked to navigate, we need to call Navigate + // here to load the home page. + preNavItemTag = "ButtonsPage"; + NavView_Navigate(preNavItemTag); + } + + private void On_Navigated(object sender, NavigationEventArgs e) + { + navView.IsBackEnabled = ContentFrame.CanGoBack; + + if (ContentFrame.SourcePageType is not null) + { + var preNavPageType = ContentFrame.CurrentSourcePageType; + var preNavPageName = preNavPageType.Name; + + var NavViewItem = navView.MenuItems + .OfType().FirstOrDefault(n => n.Tag.Equals(preNavPageName)); + + if (!(NavViewItem is null)) + navView.SelectedItem = NavViewItem; + + string header = currentTemplate.Product.Length > 0 ? + "Profile: " + currentTemplate.Product : "Layout: Desktop"; + parentNavView.Header = new TextBlock() { Text = header }; + } + } + + private void CheckBoxDefaultLayout_Checked(object sender, RoutedEventArgs e) + { + var isDefaultLayout = (bool)CheckBoxDefaultLayout.IsChecked; + var prevDefaultLayoutProfile = ProfileManager.GetProfileWithDefaultLayout(); + + currentTemplate.Layout.IsDefaultLayout = isDefaultLayout; + currentTemplate.Layout.UpdateLayout(); + + // If option is enabled and a different default layout profile exists, we want to set option false on prev profile. + if (isDefaultLayout && prevDefaultLayoutProfile != null && prevDefaultLayoutProfile.Layout != currentTemplate.Layout) + { + prevDefaultLayoutProfile.Layout.IsDefaultLayout = false; + ProfileManager.UpdateOrCreateProfile(prevDefaultLayoutProfile); + } + } } \ No newline at end of file diff --git a/HandheldCompanion/Views/Pages/NotificationsPage.xaml.cs b/HandheldCompanion/Views/Pages/NotificationsPage.xaml.cs index c4d736afb..42ad498c0 100644 --- a/HandheldCompanion/Views/Pages/NotificationsPage.xaml.cs +++ b/HandheldCompanion/Views/Pages/NotificationsPage.xaml.cs @@ -1,73 +1,73 @@ -using HandheldCompanion.Controls.Hints; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Timers; -using System.Windows; -using Page = System.Windows.Controls.Page; - -namespace HandheldCompanion.Views.Pages -{ - /// - /// Interaction logic for NotificationsPage.xaml - /// - public partial class NotificationsPage : Page - { - public delegate void StatusChangedEventHandler(int status); - public event StatusChangedEventHandler StatusChanged; - - private Timer timer; - private int prevStatus = -1; - - public NotificationsPage() - { - InitializeComponent(); - } - - public NotificationsPage(string Tag) : this() - { - this.Tag = Tag; - - timer = new(1000); - timer.Elapsed += Timer_Elapsed; - } - - private void Timer_Elapsed(object? sender, ElapsedEventArgs e) - { - // UI thread (async) - Application.Current.Dispatcher.Invoke(() => - { - bool hasAnyVisible = Notifications.Children.OfType().Any(element => element.Visibility == Visibility.Visible); - NothingToSee.Visibility = hasAnyVisible ? Visibility.Collapsed : Visibility.Visible; - - int status = Convert.ToInt32(hasAnyVisible); - if (status != prevStatus) - { - StatusChanged?.Invoke(status); - prevStatus = status; - } - }); - } - - private void Page_Loaded(object sender, RoutedEventArgs e) - { - } - - public void Page_Closed() - { - // UI thread (async) - Application.Current.Dispatcher.Invoke(() => - { - IEnumerable notifications = Notifications.Children.OfType(); - foreach (IHint hint in notifications) - hint?.Stop(); - }); - } - - private void Page_LayoutUpdated(object sender, EventArgs e) - { - timer.Stop(); - timer.Start(); - } - } -} +using HandheldCompanion.Controls.Hints; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Timers; +using System.Windows; +using Page = System.Windows.Controls.Page; + +namespace HandheldCompanion.Views.Pages +{ + /// + /// Interaction logic for NotificationsPage.xaml + /// + public partial class NotificationsPage : Page + { + public delegate void StatusChangedEventHandler(int status); + public event StatusChangedEventHandler StatusChanged; + + private Timer timer; + private int prevStatus = -1; + + public NotificationsPage() + { + InitializeComponent(); + } + + public NotificationsPage(string Tag) : this() + { + this.Tag = Tag; + + timer = new(1000); + timer.Elapsed += Timer_Elapsed; + } + + private void Timer_Elapsed(object? sender, ElapsedEventArgs e) + { + // UI thread (async) + Application.Current.Dispatcher.Invoke(() => + { + bool hasAnyVisible = Notifications.Children.OfType().Any(element => element.Visibility == Visibility.Visible); + NothingToSee.Visibility = hasAnyVisible ? Visibility.Collapsed : Visibility.Visible; + + int status = Convert.ToInt32(hasAnyVisible); + if (status != prevStatus) + { + StatusChanged?.Invoke(status); + prevStatus = status; + } + }); + } + + private void Page_Loaded(object sender, RoutedEventArgs e) + { + } + + public void Page_Closed() + { + // UI thread (async) + Application.Current.Dispatcher.Invoke(() => + { + IEnumerable notifications = Notifications.Children.OfType(); + foreach (IHint hint in notifications) + hint?.Stop(); + }); + } + + private void Page_LayoutUpdated(object sender, EventArgs e) + { + timer.Stop(); + timer.Start(); + } + } +} diff --git a/HandheldCompanion/Views/Pages/OverlayPage.xaml b/HandheldCompanion/Views/Pages/OverlayPage.xaml index 6f11b32be..160006e33 100644 --- a/HandheldCompanion/Views/Pages/OverlayPage.xaml +++ b/HandheldCompanion/Views/Pages/OverlayPage.xaml @@ -23,7 +23,6 @@ - - - + + + Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}" + Style="{StaticResource CaptionTextBlockStyle}" + Text="{x:Static resx:Resources.OverlayPage_TimeLevelDesc}" + TextWrapping="Wrap" /> + Name="ComboBoxOnScreenDisplayTimeLevel" + Grid.Column="1" + Margin="12,0,0,0" + HorizontalAlignment="Stretch" + IsReadOnly="True" + SelectionChanged="ComboBoxOnScreenDisplayTimeLevel_SelectionChanged"> @@ -187,20 +184,20 @@ + Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}" + Style="{StaticResource CaptionTextBlockStyle}" + Text="{x:Static resx:Resources.OverlayPage_FPSLevelDesc}" + TextWrapping="Wrap" /> + Name="ComboBoxOnScreenDisplayFPSLevel" + Grid.Column="1" + Margin="12,0,0,0" + HorizontalAlignment="Stretch" + IsReadOnly="True" + SelectionChanged="ComboBoxOnScreenDisplayFPSLevel_SelectionChanged"> @@ -224,20 +221,20 @@ + Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}" + Style="{StaticResource CaptionTextBlockStyle}" + Text="{x:Static resx:Resources.OverlayPage_CPULevelDesc}" + TextWrapping="Wrap" /> + Name="ComboBoxOnScreenDisplayCPULevel" + Grid.Column="1" + Margin="12,0,0,0" + HorizontalAlignment="Stretch" + IsReadOnly="True" + SelectionChanged="ComboBoxOnScreenDisplayCPULevel_SelectionChanged"> @@ -247,9 +244,9 @@ + Padding="15,12,12,12" + Background="{DynamicResource ExpanderHeaderBackground}" + CornerRadius="{DynamicResource ControlCornerRadius}"> @@ -261,20 +258,20 @@ + Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}" + Style="{StaticResource CaptionTextBlockStyle}" + Text="{x:Static resx:Resources.OverlayPage_RAMLevelDesc}" + TextWrapping="Wrap" /> + Name="ComboBoxOnScreenDisplayRAMLevel" + Grid.Column="1" + Margin="12,0,0,0" + HorizontalAlignment="Stretch" + IsReadOnly="True" + SelectionChanged="ComboBoxOnScreenDisplayRAMLevel_SelectionChanged"> @@ -284,9 +281,9 @@ + Padding="15,12,12,12" + Background="{DynamicResource ExpanderHeaderBackground}" + CornerRadius="{DynamicResource ControlCornerRadius}"> @@ -298,20 +295,20 @@ + Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}" + Style="{StaticResource CaptionTextBlockStyle}" + Text="{x:Static resx:Resources.OverlayPage_GPULevelDesc}" + TextWrapping="Wrap" /> + Name="ComboBoxOnScreenDisplayGPULevel" + Grid.Column="1" + Margin="12,0,0,0" + HorizontalAlignment="Stretch" + IsReadOnly="True" + SelectionChanged="ComboBoxOnScreenDisplayGPULevel_SelectionChanged"> @@ -321,9 +318,9 @@ + Padding="15,12,12,12" + Background="{DynamicResource ExpanderHeaderBackground}" + CornerRadius="{DynamicResource ControlCornerRadius}"> @@ -335,20 +332,20 @@ + Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}" + Style="{StaticResource CaptionTextBlockStyle}" + Text="{x:Static resx:Resources.OverlayPage_VRAMLevelDesc}" + TextWrapping="Wrap" /> + Name="ComboBoxOnScreenDisplayVRAMLevel" + Grid.Column="1" + Margin="12,0,0,0" + HorizontalAlignment="Stretch" + IsReadOnly="True" + SelectionChanged="ComboBoxOnScreenDisplayVRAMLevel_SelectionChanged"> @@ -358,9 +355,9 @@ + Padding="15,12,12,12" + Background="{DynamicResource ExpanderHeaderBackground}" + CornerRadius="{DynamicResource ControlCornerRadius}"> @@ -372,20 +369,20 @@ + Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}" + Style="{StaticResource CaptionTextBlockStyle}" + Text="{x:Static resx:Resources.OverlayPage_BATTLevelDesc}" + TextWrapping="Wrap" /> + Name="ComboBoxOnScreenDisplayBATTLevel" + Grid.Column="1" + Margin="12,0,0,0" + HorizontalAlignment="Stretch" + IsReadOnly="True" + SelectionChanged="ComboBoxOnScreenDisplayBATTLevel_SelectionChanged"> @@ -436,15 +433,14 @@ HorizontalAlignment="Stretch" VerticalAlignment="Center" SelectionChanged="OverlayModel_SelectionChanged"> - - - - + + - - - + + + + @@ -740,6 +736,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -881,113 +976,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HandheldCompanion/Views/Pages/PerformancePage.xaml b/HandheldCompanion/Views/Pages/PerformancePage.xaml index 68f7a9686..6ed554eac 100644 --- a/HandheldCompanion/Views/Pages/PerformancePage.xaml +++ b/HandheldCompanion/Views/Pages/PerformancePage.xaml @@ -1,914 +1,846 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/HandheldCompanion/Views/QuickPages/QuickPerformancePage.xaml.cs b/HandheldCompanion/Views/QuickPages/QuickPerformancePage.xaml.cs index 36fb38541..64bc1f610 100644 --- a/HandheldCompanion/Views/QuickPages/QuickPerformancePage.xaml.cs +++ b/HandheldCompanion/Views/QuickPages/QuickPerformancePage.xaml.cs @@ -1,515 +1,21 @@ -using HandheldCompanion.Devices; -using HandheldCompanion.Managers; -using HandheldCompanion.Managers.Desktop; -using HandheldCompanion.Misc; -using HandheldCompanion.Platforms; -using HandheldCompanion.Processors; -using HandheldCompanion.Utils; -using HandheldCompanion.Views.Windows; -using iNKORE.UI.WPF.Modern.Controls; -using System; -using System.Timers; -using System.Windows; -using System.Windows.Controls; -using Page = System.Windows.Controls.Page; - -namespace HandheldCompanion.Views.QuickPages; - -/// -/// Interaction logic for QuickPerformancePage.xaml -/// -public partial class QuickPerformancePage : Page -{ - private const int UpdateInterval = 500; - private readonly Timer UpdateTimer; - private PowerProfile selectedProfile; - - private LockObject updateLock = new(); - - public QuickPerformancePage(string Tag) : this() - { - this.Tag = Tag; - } - - public QuickPerformancePage() - { - InitializeComponent(); - - /* - PerformanceManager.PowerModeChanged += PerformanceManager_PowerModeChanged; - PerformanceManager.PerfBoostModeChanged += PerformanceManager_PerfBoostModeChanged; - PerformanceManager.EPPChanged += PerformanceManager_EPPChanged; - */ - - // manage events - SettingsManager.SettingValueChanged += SettingsManager_SettingValueChanged; - PlatformManager.RTSS.Updated += RTSS_Updated; - PerformanceManager.ProcessorStatusChanged += PerformanceManager_StatusChanged; - PerformanceManager.EPPChanged += PerformanceManager_EPPChanged; - PerformanceManager.Initialized += PerformanceManager_Initialized; - PowerProfileManager.Updated += PowerProfileManager_Updated; - PowerProfileManager.Deleted += PowerProfileManager_Deleted; - MultimediaManager.PrimaryScreenChanged += SystemManager_PrimaryScreenChanged; - - // device settings - GPUSlider.Minimum = MainWindow.CurrentDevice.GfxClock[0]; - GPUSlider.Maximum = MainWindow.CurrentDevice.GfxClock[1]; - - CPUSlider.Minimum = MotherboardInfo.ProcessorMaxTurboSpeed / 4.0d; - CPUSlider.Maximum = MotherboardInfo.ProcessorMaxTurboSpeed; - - // motherboard settings - CPUCoreSlider.Maximum = MotherboardInfo.NumberOfCores; - - FanModeSoftware.IsEnabled = MainWindow.CurrentDevice.Capabilities.HasFlag(DeviceCapabilities.FanControl); - - UpdateTimer = new Timer(UpdateInterval); - UpdateTimer.AutoReset = false; - UpdateTimer.Elapsed += (sender, e) => SubmitProfile(); - - // force call - RTSS_Updated(PlatformManager.RTSS.Status); - } - - private void SystemManager_PrimaryScreenChanged(DesktopScreen desktopScreen) - { - // UI thread (async) - Application.Current.Dispatcher.BeginInvoke(() => - { - AutoTDPSlider.Maximum = desktopScreen.devMode.dmDisplayFrequency; - }); - } - - private void PowerProfileManager_Deleted(PowerProfile profile) - { - // current power profile deleted, return to previous page - bool isCurrent = selectedProfile?.Guid == profile.Guid; - if (isCurrent) - MainWindow.overlayquickTools.ContentFrame.GoBack(); - } - - private void PowerProfileManager_Updated(PowerProfile profile, UpdateSource source) - { - if (selectedProfile is null) - return; - - // UI thread (async) - Application.Current.Dispatcher.BeginInvoke(() => - { - // current power profile updated, update UI - bool isCurrent = selectedProfile.Guid == profile.Guid; - if (isCurrent) - UpdateUI(); - }); - } - - private void RTSS_Updated(PlatformStatus status) - { - // UI thread (async) - Application.Current.Dispatcher.BeginInvoke(() => - { - switch (status) - { - case PlatformStatus.Ready: - var Processor = PerformanceManager.GetProcessor(); - StackProfileAutoTDP.IsEnabled = true && Processor is not null ? Processor.CanChangeTDP : false; - break; - case PlatformStatus.Stalled: - // StackProfileFramerate.IsEnabled = false; - // StackProfileAutoTDP.IsEnabled = false; - break; - } - }); - } - - public void UpdateProfile() - { - if (UpdateTimer is not null) - { - UpdateTimer.Stop(); - UpdateTimer.Start(); - } - } - - public void SubmitProfile(UpdateSource source = UpdateSource.ProfilesPage) - { - if (selectedProfile is null) - return; - - PowerProfileManager.UpdateOrCreateProfile(selectedProfile, source); - } - - private void PerformanceManager_StatusChanged(bool CanChangeTDP, bool CanChangeGPU) - { - // UI thread (async) - Application.Current.Dispatcher.BeginInvoke(() => - { - StackProfileTDP.IsEnabled = CanChangeTDP; - StackProfileAutoTDP.IsEnabled = CanChangeTDP && PlatformManager.RTSS.IsInstalled; - - StackProfileGPUClock.IsEnabled = CanChangeGPU; - }); - } - - private void PerformanceManager_EPPChanged(uint EPP) - { - // UI thread (async) - Application.Current.Dispatcher.BeginInvoke(() => - { - EPPSlider.Value = EPP; - }); - } - - private void PerformanceManager_Initialized() - { - Processor processor = PerformanceManager.GetProcessor(); - if (processor is null) - return; - - PerformanceManager_StatusChanged(processor.CanChangeTDP, processor.CanChangeGPU); - } - - /* - private void PerformanceManager_PowerModeChanged(int idx) - { - // UI thread (async) - Application.Current.Dispatcher.BeginInvoke(() => { PowerModeSlider.Value = idx; }); - } - - private void PerformanceManager_PerfBoostModeChanged(bool value) - { - // UI thread (async) - Application.Current.Dispatcher.BeginInvoke(() => { CPUBoostToggle.IsOn = value; }); - } - */ - - private void SettingsManager_SettingValueChanged(string name, object value) - { - // UI thread (async) - Application.Current.Dispatcher.BeginInvoke(() => - { - switch (name) - { - case "ConfigurableTDPOverrideDown": - { - using (new ScopedLock(updateLock)) - { - TDPSlider.Minimum = (double)value; - } - } - break; - case "ConfigurableTDPOverrideUp": - { - using (new ScopedLock(updateLock)) - { - TDPSlider.Maximum = (double)value; - } - } - break; - } - }); - } - - private void PowerMode_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (PowerMode.SelectedIndex == -1) - return; - - if (selectedProfile is null) - return; - - // wait until lock is released - if (updateLock) - return; - - selectedProfile.OSPowerMode = PerformanceManager.PowerModes[PowerMode.SelectedIndex]; - UpdateProfile(); - } - - private void CPUBoostLevel_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (CPUBoostLevel.SelectedIndex == -1) - return; - - if (selectedProfile is null) - return; - - if (updateLock) - return; - - selectedProfile.CPUBoostLevel = CPUBoostLevel.SelectedIndex; - UpdateProfile(); - } - - public void SelectionChanged(Guid guid) - { - // if an update is pending, cut it short, it will disturb profile selection though - // keep me ? - if (UpdateTimer.Enabled) - { - UpdateTimer.Stop(); - SubmitProfile(); - } - - selectedProfile = PowerProfileManager.GetProfile(guid); - UpdateUI(); - } - - private void UpdateUI() - { - if (selectedProfile is null) - return; - - // UI thread (async) - Application.Current.Dispatcher.BeginInvoke(() => - { - using (new ScopedLock(updateLock)) - { - // we shouldn't allow users to modify some of default profile settings - PowerSettingsPanel.IsEnabled = !selectedProfile.DeviceDefault; - Button_PowerSettings_Delete.IsEnabled = !selectedProfile.Default; - - // page name - this.Title = selectedProfile.Name; - - // TDP - TDPToggle.IsOn = selectedProfile.TDPOverrideEnabled; - var TDP = selectedProfile.TDPOverrideValues is not null - ? selectedProfile.TDPOverrideValues - : MainWindow.CurrentDevice.nTDP; - TDPSlider.Value = TDP[(int)PowerType.Slow]; - - // CPU Clock control - CPUToggle.IsOn = selectedProfile.CPUOverrideEnabled; - CPUSlider.Value = selectedProfile.CPUOverrideValue != 0 ? selectedProfile.CPUOverrideValue : MotherboardInfo.ProcessorMaxTurboSpeed; - - // GPU Clock control - GPUToggle.IsOn = selectedProfile.GPUOverrideEnabled; - GPUSlider.Value = selectedProfile.GPUOverrideValue != 0 ? selectedProfile.GPUOverrideValue : 255 * 50; - - // AutoTDP - AutoTDPToggle.IsOn = selectedProfile.AutoTDPEnabled; - AutoTDPSlider.Value = selectedProfile.AutoTDPRequestedFPS; - - // EPP - EPPToggle.IsOn = selectedProfile.EPPOverrideEnabled; - EPPSlider.Value = selectedProfile.EPPOverrideValue; - - // CPU Core Count - CPUCoreToggle.IsOn = selectedProfile.CPUCoreEnabled; - CPUCoreSlider.Value = selectedProfile.CPUCoreCount; - - // CPU Boost - CPUBoostLevel.SelectedIndex = selectedProfile.CPUBoostLevel; - - // Power Mode - PowerMode.SelectedIndex = Array.IndexOf(PerformanceManager.PowerModes, selectedProfile.OSPowerMode); - - // Fan control - FanMode.SelectedIndex = (int)selectedProfile.FanProfile.fanMode; - } - }); - } - - private void TDPToggle_Toggled(object sender, RoutedEventArgs e) - { - if (selectedProfile is null) - return; - - if (updateLock) - return; - - selectedProfile.TDPOverrideEnabled = TDPToggle.IsOn; - selectedProfile.TDPOverrideValues = new double[3] - { - TDPSlider.Value, - TDPSlider.Value, - TDPSlider.Value - }; - - UpdateProfile(); - } - - private void TDPSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) - { - if (selectedProfile is null) - return; - - if (updateLock) - return; - - selectedProfile.TDPOverrideValues = new double[3] - { - TDPSlider.Value, - TDPSlider.Value, - TDPSlider.Value - }; - - UpdateProfile(); - } - - private void AutoTDPToggle_Toggled(object sender, RoutedEventArgs e) - { - if (selectedProfile is null) - return; - - if (updateLock) - return; - - selectedProfile.AutoTDPEnabled = AutoTDPToggle.IsOn; - AutoTDPSlider.Value = selectedProfile.AutoTDPRequestedFPS; - - UpdateProfile(); - } - - private void AutoTDPRequestedFPSSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) - { - if (selectedProfile is null) - return; - - if (updateLock) - return; - - selectedProfile.AutoTDPRequestedFPS = (int)AutoTDPSlider.Value; - UpdateProfile(); - } - - private void CPUToggle_Toggled(object sender, RoutedEventArgs e) - { - if (selectedProfile is null) - return; - - if (updateLock) - return; - - selectedProfile.CPUOverrideEnabled = CPUToggle.IsOn; - selectedProfile.CPUOverrideValue = (int)CPUSlider.Value; - UpdateProfile(); - } - - private void CPUSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) - { - if (selectedProfile is null) - return; - - if (updateLock) - return; - - selectedProfile.CPUOverrideValue = (int)CPUSlider.Value; - UpdateProfile(); - } - - private void GPUToggle_Toggled(object sender, RoutedEventArgs e) - { - if (selectedProfile is null) - return; - - if (updateLock) - return; - - selectedProfile.GPUOverrideEnabled = GPUToggle.IsOn; - selectedProfile.GPUOverrideValue = (int)GPUSlider.Value; - UpdateProfile(); - } - - private void GPUSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) - { - if (selectedProfile is null) - return; - - if (updateLock) - return; - - selectedProfile.GPUOverrideValue = (int)GPUSlider.Value; - UpdateProfile(); - } - - private void EPPToggle_Toggled(object sender, RoutedEventArgs e) - { - if (selectedProfile is null) - return; - - if (updateLock) - return; - - selectedProfile.EPPOverrideEnabled = EPPToggle.IsOn; - selectedProfile.EPPOverrideValue = (uint)EPPSlider.Value; - UpdateProfile(); - } - - private void EPPSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) - { - if (selectedProfile is null) - return; - - if (updateLock) - return; - - selectedProfile.EPPOverrideValue = (uint)EPPSlider.Value; - UpdateProfile(); - } - - private void CPUCoreToggle_Toggled(object sender, RoutedEventArgs e) - { - if (selectedProfile is null) - return; - - // wait until lock is released - if (updateLock) - return; - - selectedProfile.CPUCoreEnabled = CPUCoreToggle.IsOn; - selectedProfile.CPUCoreCount = (int)CPUCoreSlider.Value; - UpdateProfile(); - } - - private void CPUCoreSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) - { - if (selectedProfile is null) - return; - - if (!CPUCoreSlider.IsInitialized) - return; - - // wait until lock is released - if (updateLock) - return; - - selectedProfile.CPUCoreCount = (int)CPUCoreSlider.Value; - UpdateProfile(); - } - - private void FanMode_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (FanMode.SelectedIndex == -1) - return; - - if (selectedProfile is null) - return; - - // wait until lock is released - if (updateLock) - return; - - selectedProfile.FanProfile.fanMode = (FanMode)FanMode.SelectedIndex; - UpdateProfile(); - } - - private async void Button_PowerSettings_Delete_Click(object sender, RoutedEventArgs e) - { - var result = Dialog.ShowAsync( - $"{Properties.Resources.ProfilesPage_AreYouSureDelete1} \"{selectedProfile.Name}\"?", - $"{Properties.Resources.ProfilesPage_AreYouSureDelete2}", - ContentDialogButton.Primary, - $"{Properties.Resources.ProfilesPage_Cancel}", - $"{Properties.Resources.ProfilesPage_Delete}", string.Empty, OverlayQuickTools.GetCurrent()); - await result; // sync call - - switch (result.Result) - { - case ContentDialogResult.Primary: - PowerProfileManager.DeleteProfile(selectedProfile); - break; - } - } +using HandheldCompanion.Managers; +using HandheldCompanion.ViewModels; +using System; +using Page = System.Windows.Controls.Page; + +namespace HandheldCompanion.Views.QuickPages; + +public partial class QuickPerformancePage : Page +{ + public QuickPerformancePage() + { + Tag = "quickperformance"; + DataContext = new PerformancePageViewModel(isQuickTools: true); + InitializeComponent(); + } + + public void SelectionChanged(Guid guid) + { + ((PerformancePageViewModel)DataContext).SelectedPreset = PowerProfileManager.GetProfile(guid); + } } \ No newline at end of file diff --git a/HandheldCompanion/Views/Windows/OverlayModel.xaml b/HandheldCompanion/Views/Windows/OverlayModel.xaml index 902f63107..210d54ad3 100644 --- a/HandheldCompanion/Views/Windows/OverlayModel.xaml +++ b/HandheldCompanion/Views/Windows/OverlayModel.xaml @@ -1,44 +1,44 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/HandheldCompanion/Views/Windows/OverlayQuickTools.xaml b/HandheldCompanion/Views/Windows/OverlayQuickTools.xaml index 95e2e2411..af629ee99 100644 --- a/HandheldCompanion/Views/Windows/OverlayQuickTools.xaml +++ b/HandheldCompanion/Views/Windows/OverlayQuickTools.xaml @@ -1,83 +1,83 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/HandheldCompanion/Views/Windows/OverlayQuickTools.xaml.cs b/HandheldCompanion/Views/Windows/OverlayQuickTools.xaml.cs index 0a9372ee5..a398194dc 100644 --- a/HandheldCompanion/Views/Windows/OverlayQuickTools.xaml.cs +++ b/HandheldCompanion/Views/Windows/OverlayQuickTools.xaml.cs @@ -1,507 +1,507 @@ -using HandheldCompanion.Managers; -using HandheldCompanion.Managers.Desktop; -using HandheldCompanion.Utils; -using HandheldCompanion.Views.Classes; -using HandheldCompanion.Views.QuickPages; -using iNKORE.UI.WPF.Modern.Controls; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; -using System.Linq; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Forms; -using System.Windows.Input; -using System.Windows.Interop; -using System.Windows.Media; -using System.Windows.Navigation; -using System.Windows.Threading; -using Windows.System.Power; -using WpfScreenHelper; -using WpfScreenHelper.Enum; -using Application = System.Windows.Application; -using KeyEventArgs = System.Windows.Input.KeyEventArgs; -using Page = System.Windows.Controls.Page; -using PowerLineStatus = System.Windows.Forms.PowerLineStatus; -using Screen = WpfScreenHelper.Screen; -using SystemManager = HandheldCompanion.Managers.SystemManager; -using SystemPowerManager = Windows.System.Power.PowerManager; -using Timer = System.Timers.Timer; - -namespace HandheldCompanion.Views.Windows; - -/// -/// Interaction logic for QuickTools.xaml -/// -public partial class OverlayQuickTools : GamepadWindow -{ - private const int SC_MOVE = 0xF010; - private readonly IntPtr HWND_TOPMOST = new IntPtr(-1); - - const UInt32 SWP_NOSIZE = 0x0001; - const UInt32 SWP_NOMOVE = 0x0002; - const UInt32 SWP_NOACTIVATE = 0x0010; - const UInt32 SWP_NOZORDER = 0x0004; - - // Define the Win32 API constants and functions - const int WM_PAINT = 0x000F; - const int WM_ACTIVATEAPP = 0x001C; - const int WM_ACTIVATE = 0x0006; - const int WM_SETFOCUS = 0x0007; - const int WM_KILLFOCUS = 0x0008; - const int WM_NCACTIVATE = 0x0086; - const int WM_SYSCOMMAND = 0x0112; - const int WM_WINDOWPOSCHANGING = 0x0046; - const int WM_SHOWWINDOW = 0x0018; - const int WM_MOUSEACTIVATE = 0x0021; - - const int WS_EX_NOACTIVATE = 0x08000000; - const int GWL_EXSTYLE = -20; - - private HwndSource hwndSource; - - private Dictionary cacheModes = new(); - private Timer WM_PAINT_TIMER; - - // page vars - private readonly Dictionary _pages = new(); - - private bool AutoHide; - private bool isClosing; - private readonly DispatcherTimer clockUpdateTimer; - - public QuickHomePage homePage; - public QuickDevicePage devicePage; - public QuickPerformancePage performancePage; - public QuickProfilesPage profilesPage; - public QuickOverlayPage overlayPage; - public QuickSuspenderPage suspenderPage; - - private static OverlayQuickTools CurrentWindow; - private string preNavItemTag; - - public OverlayQuickTools() - { - InitializeComponent(); - CurrentWindow = this; - - // used by gamepad navigation - Tag = "QuickTools"; - - PreviewKeyDown += HandleEsc; - - clockUpdateTimer = new DispatcherTimer(); - clockUpdateTimer.Interval = TimeSpan.FromMilliseconds(500); - clockUpdateTimer.Tick += UpdateTime; - - WM_PAINT_TIMER = new(250) { AutoReset = false }; - WM_PAINT_TIMER.Elapsed += WM_PAINT_TIMER_Tick; - - // create manager(s) - SystemManager.PowerStatusChanged += PowerManager_PowerStatusChanged; - - MultimediaManager.DisplaySettingsChanged += SystemManager_DisplaySettingsChanged; - SettingsManager.SettingValueChanged += SettingsManager_SettingValueChanged; - - // create pages - homePage = new("quickhome"); - devicePage = new("quickdevice"); - profilesPage = new("quickprofiles"); - suspenderPage = new("quicksuspender"); - - _pages.Add("QuickHomePage", homePage); - _pages.Add("QuickDevicePage", devicePage); - _pages.Add("QuickProfilesPage", profilesPage); - _pages.Add("QuickSuspenderPage", suspenderPage); - } - - public void LoadPages_MVVM() - { - overlayPage = new QuickOverlayPage(); - performancePage = new QuickPerformancePage(); - - _pages.Add("QuickOverlayPage", overlayPage); - _pages.Add("QuickPerformancePage", performancePage); - } - - public static OverlayQuickTools GetCurrent() - { - return CurrentWindow; - } - - private void SettingsManager_SettingValueChanged(string name, object value) - { - // UI thread - Application.Current.Dispatcher.Invoke(() => - { - switch (name) - { - case "QuickToolsLocation": - { - var QuickToolsLocation = Convert.ToInt32(value); - UpdateLocation(QuickToolsLocation); - } - break; - case "QuickToolsAutoHide": - { - AutoHide = Convert.ToBoolean(value); - } - break; - } - }); - } - - private void SystemManager_DisplaySettingsChanged(DesktopScreen desktopScreen, ScreenResolution resolution) - { - int QuickToolsLocation = SettingsManager.GetInt("QuickToolsLocation"); - UpdateLocation(QuickToolsLocation); - } - - private void UpdateLocation(int QuickToolsLocation) - { - // UI thread (async) - Application.Current.Dispatcher.Invoke(() => - { - switch (QuickToolsLocation) - { - // top, left - // bottom, left - case 0: - case 2: - this.SetWindowPosition(WindowPositions.Left, Screen.PrimaryScreen); - Left += Margin.Left; - break; - - // top, right - // bottom, right - default: - case 1: - case 3: - this.SetWindowPosition(WindowPositions.Right, Screen.PrimaryScreen); - Left -= Margin.Right; - break; - } - - Height = MinHeight = MaxHeight = (int)(Screen.PrimaryScreen.WpfBounds.Height - (2.0d * Margin.Top)); - Top = Margin.Top; - }); - } - - private void PowerManager_PowerStatusChanged(PowerStatus status) - { - // UI thread (async) - Application.Current.Dispatcher.Invoke(() => - { - var BatteryLifePercent = (int)Math.Truncate(status.BatteryLifePercent * 100.0f); - BatteryIndicatorPercentage.Text = $"{BatteryLifePercent}%"; - - // get status key - var KeyStatus = string.Empty; - switch (status.PowerLineStatus) - { - case PowerLineStatus.Online: - KeyStatus = "Charging"; - break; - default: - { - var energy = SystemPowerManager.EnergySaverStatus; - switch (energy) - { - case EnergySaverStatus.On: - KeyStatus = "Saver"; - break; - } - } - break; - } - - // get battery key - var KeyValue = (int)Math.Truncate(status.BatteryLifePercent * 10); - - // set key - var Key = $"Battery{KeyStatus}{KeyValue}"; - - if (SystemManager.PowerStatusIcon.TryGetValue(Key, out var glyph)) - BatteryIndicatorIcon.Glyph = glyph; - - if (status.BatteryLifeRemaining > 0) - { - var time = TimeSpan.FromSeconds(status.BatteryLifeRemaining); - - string remaining; - if (status.BatteryLifeRemaining >= 3600) - remaining = $"{time.Hours}h {time.Minutes}min"; - else - remaining = $"{time.Minutes}min"; - - BatteryIndicatorLifeRemaining.Text = $"({remaining} remaining)"; - BatteryIndicatorLifeRemaining.Visibility = Visibility.Visible; - } - else - { - BatteryIndicatorLifeRemaining.Text = string.Empty; - BatteryIndicatorLifeRemaining.Visibility = Visibility.Collapsed; - } - }); - } - - private void Window_Loaded(object sender, RoutedEventArgs e) - { - // load gamepad navigation maanger - gamepadFocusManager = new(this, ContentFrame); - - hwndSource = PresentationSource.FromVisual(this) as HwndSource; - hwndSource.AddHook(WndProc); - - if (hwndSource != null) - { - hwndSource.CompositionTarget.RenderMode = RenderMode.SoftwareOnly; - WinAPI.SetWindowPos(hwndSource.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE); - } - } - - private IntPtr prevWParam = new(0x0000000000000086); - private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) - { - switch (msg) - { - case WM_SYSCOMMAND: - { - var command = wParam.ToInt32() & 0xfff0; - if (command == SC_MOVE) handled = true; - } - break; - - case WM_SETFOCUS: - { - if (hwndSource != null) - WinAPI.SetWindowPos(hwndSource.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE); - handled = true; - } - break; - - case WM_NCACTIVATE: - { - // prevent window from loosing its fancy style - if (wParam == 0 && (lParam == 0)) - { - if (prevWParam != new IntPtr(0x0000000000000086)) - if (AutoHide && Visibility == Visibility.Visible) - ToggleVisibility(); - handled = true; - } - - if (wParam == 1) - { - handled = true; - } - - prevWParam = wParam; - } - break; - - case WM_ACTIVATEAPP: - { - if (wParam == 0) - { - if (hwndSource != null) - WPFUtils.SendMessage(hwndSource.Handle, WM_NCACTIVATE, WM_NCACTIVATE, 0); - } - } - break; - - case WM_ACTIVATE: - { - // WA_INACTIVE - if (wParam == 0) - handled = true; - } - break; - - case WM_MOUSEACTIVATE: - { - handled = true; - } - break; - - case WM_PAINT: - { - // Loop through all visual elements in the window - foreach (var element in WPFUtils.FindVisualChildren(this)) - { - if (element.CacheMode is not null) - { - // Store the previous CacheMode value - cacheModes[element] = element.CacheMode.Clone(); - - // Set the CacheMode to null - element.CacheMode = null; - } - } - - WM_PAINT_TIMER.Stop(); - WM_PAINT_TIMER.Start(); - } - break; - } - - return IntPtr.Zero; - } - - private void WM_PAINT_TIMER_Tick(object? sender, EventArgs e) - { - // UI thread - Application.Current.Dispatcher.Invoke(() => - { - // Set the CacheMode back to the previous value - foreach (UIElement element in cacheModes.Keys) - element.CacheMode = cacheModes[element]; - }); - } - - private void HandleEsc(object sender, KeyEventArgs e) - { - if (e.Key == Key.Escape) - ToggleVisibility(); - } - - public void ToggleVisibility() - { - // UI thread - Application.Current.Dispatcher.Invoke(() => - { - switch (Visibility) - { - case Visibility.Collapsed: - case Visibility.Hidden: - Show(); - Focus(); - - if (hwndSource != null) - WPFUtils.SendMessage(hwndSource.Handle, WM_NCACTIVATE, WM_NCACTIVATE, 0); - - InvokeGotGamepadWindowFocus(); - - clockUpdateTimer.Start(); - break; - case Visibility.Visible: - Hide(); - - InvokeLostGamepadWindowFocus(); - - clockUpdateTimer.Stop(); - break; - } - }); - } - - private void Window_Closing(object sender, CancelEventArgs e) - { - e.Cancel = !isClosing; - - if (!isClosing) - ToggleVisibility(); - else - { - // close pages - devicePage.Close(); - } - } - - public void Close(bool v) - { - isClosing = v; - Close(); - } - - #region navView - - private void navView_ItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args) - { - if (args.InvokedItemContainer is not null) - { - var navItem = (NavigationViewItem)args.InvokedItemContainer; - var navItemTag = (string)navItem.Tag; - - switch (navItemTag) - { - default: - preNavItemTag = navItemTag; - break; - case "shortcutKeyboard": - case "shortcutDesktop": - case "shortcutESC": - case "shortcutExpand": - HotkeysManager.TriggerRaised(navItemTag, null, 0, false, true); - break; - } - - NavView_Navigate(preNavItemTag); - } - } - - public void NavView_Navigate(string navItemTag) - { - var item = _pages.FirstOrDefault(p => p.Key.Equals(navItemTag)); - var _page = item.Value; - - // Get the page type before navigation so you can prevent duplicate - // entries in the backstack. - var preNavPageType = ContentFrame.CurrentSourcePageType; - - // Only navigate if the selected page isn't currently loaded. - if (!(_page is null) && !Equals(preNavPageType, _page)) NavView_Navigate(_page); - } - - public void NavView_Navigate(Page _page) - { - ContentFrame.Navigate(_page); - } - - private void navView_Loaded(object sender, RoutedEventArgs e) - { - // Add handler for ContentFrame navigation. - ContentFrame.Navigated += On_Navigated; - - // If navigation occurs on SelectionChanged, this isn't needed. - // Because we use ItemInvoked to navigate, we need to call Navigate - // here to load the home page. - preNavItemTag = "QuickHomePage"; - NavView_Navigate(preNavItemTag); - } - - private void navView_BackRequested(NavigationView sender, NavigationViewBackRequestedEventArgs args) - { - TryGoBack(); - } - - private bool TryGoBack() - { - if (!ContentFrame.CanGoBack) - return false; - - // Don't go back if the nav pane is overlayed. - if (navView.IsPaneOpen && - (navView.DisplayMode == NavigationViewDisplayMode.Compact || - navView.DisplayMode == NavigationViewDisplayMode.Minimal)) - return false; - - ContentFrame.GoBack(); - return true; - } - - private void On_Navigated(object sender, NavigationEventArgs e) - { - navView.IsBackEnabled = ContentFrame.CanGoBack; - navHeader.Text = ((Page)((ContentControl)sender).Content).Title; - } - - private void UpdateTime(object? sender, EventArgs e) - { - var timeFormat = CultureInfo.InstalledUICulture.DateTimeFormat.ShortTimePattern; - Time.Text = DateTime.Now.ToString(timeFormat); - } - - #endregion +using HandheldCompanion.Managers; +using HandheldCompanion.Managers.Desktop; +using HandheldCompanion.Utils; +using HandheldCompanion.Views.Classes; +using HandheldCompanion.Views.QuickPages; +using iNKORE.UI.WPF.Modern.Controls; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Forms; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Navigation; +using System.Windows.Threading; +using Windows.System.Power; +using WpfScreenHelper; +using WpfScreenHelper.Enum; +using Application = System.Windows.Application; +using KeyEventArgs = System.Windows.Input.KeyEventArgs; +using Page = System.Windows.Controls.Page; +using PowerLineStatus = System.Windows.Forms.PowerLineStatus; +using Screen = WpfScreenHelper.Screen; +using SystemManager = HandheldCompanion.Managers.SystemManager; +using SystemPowerManager = Windows.System.Power.PowerManager; +using Timer = System.Timers.Timer; + +namespace HandheldCompanion.Views.Windows; + +/// +/// Interaction logic for QuickTools.xaml +/// +public partial class OverlayQuickTools : GamepadWindow +{ + private const int SC_MOVE = 0xF010; + private readonly IntPtr HWND_TOPMOST = new IntPtr(-1); + + const UInt32 SWP_NOSIZE = 0x0001; + const UInt32 SWP_NOMOVE = 0x0002; + const UInt32 SWP_NOACTIVATE = 0x0010; + const UInt32 SWP_NOZORDER = 0x0004; + + // Define the Win32 API constants and functions + const int WM_PAINT = 0x000F; + const int WM_ACTIVATEAPP = 0x001C; + const int WM_ACTIVATE = 0x0006; + const int WM_SETFOCUS = 0x0007; + const int WM_KILLFOCUS = 0x0008; + const int WM_NCACTIVATE = 0x0086; + const int WM_SYSCOMMAND = 0x0112; + const int WM_WINDOWPOSCHANGING = 0x0046; + const int WM_SHOWWINDOW = 0x0018; + const int WM_MOUSEACTIVATE = 0x0021; + + const int WS_EX_NOACTIVATE = 0x08000000; + const int GWL_EXSTYLE = -20; + + private HwndSource hwndSource; + + private Dictionary cacheModes = new(); + private Timer WM_PAINT_TIMER; + + // page vars + private readonly Dictionary _pages = new(); + + private bool AutoHide; + private bool isClosing; + private readonly DispatcherTimer clockUpdateTimer; + + public QuickHomePage homePage; + public QuickDevicePage devicePage; + public QuickPerformancePage performancePage; + public QuickProfilesPage profilesPage; + public QuickOverlayPage overlayPage; + public QuickSuspenderPage suspenderPage; + + private static OverlayQuickTools CurrentWindow; + private string preNavItemTag; + + public OverlayQuickTools() + { + InitializeComponent(); + CurrentWindow = this; + + // used by gamepad navigation + Tag = "QuickTools"; + + PreviewKeyDown += HandleEsc; + + clockUpdateTimer = new DispatcherTimer(); + clockUpdateTimer.Interval = TimeSpan.FromMilliseconds(500); + clockUpdateTimer.Tick += UpdateTime; + + WM_PAINT_TIMER = new(250) { AutoReset = false }; + WM_PAINT_TIMER.Elapsed += WM_PAINT_TIMER_Tick; + + // create manager(s) + SystemManager.PowerStatusChanged += PowerManager_PowerStatusChanged; + + MultimediaManager.DisplaySettingsChanged += SystemManager_DisplaySettingsChanged; + SettingsManager.SettingValueChanged += SettingsManager_SettingValueChanged; + + // create pages + homePage = new("quickhome"); + devicePage = new("quickdevice"); + profilesPage = new("quickprofiles"); + suspenderPage = new("quicksuspender"); + + _pages.Add("QuickHomePage", homePage); + _pages.Add("QuickDevicePage", devicePage); + _pages.Add("QuickProfilesPage", profilesPage); + _pages.Add("QuickSuspenderPage", suspenderPage); + } + + public void LoadPages_MVVM() + { + overlayPage = new QuickOverlayPage(); + performancePage = new QuickPerformancePage(); + + _pages.Add("QuickOverlayPage", overlayPage); + _pages.Add("QuickPerformancePage", performancePage); + } + + public static OverlayQuickTools GetCurrent() + { + return CurrentWindow; + } + + private void SettingsManager_SettingValueChanged(string name, object value) + { + // UI thread + Application.Current.Dispatcher.Invoke(() => + { + switch (name) + { + case "QuickToolsLocation": + { + var QuickToolsLocation = Convert.ToInt32(value); + UpdateLocation(QuickToolsLocation); + } + break; + case "QuickToolsAutoHide": + { + AutoHide = Convert.ToBoolean(value); + } + break; + } + }); + } + + private void SystemManager_DisplaySettingsChanged(DesktopScreen desktopScreen, ScreenResolution resolution) + { + int QuickToolsLocation = SettingsManager.GetInt("QuickToolsLocation"); + UpdateLocation(QuickToolsLocation); + } + + private void UpdateLocation(int QuickToolsLocation) + { + // UI thread (async) + Application.Current.Dispatcher.Invoke(() => + { + switch (QuickToolsLocation) + { + // top, left + // bottom, left + case 0: + case 2: + this.SetWindowPosition(WindowPositions.Left, Screen.PrimaryScreen); + Left += Margin.Left; + break; + + // top, right + // bottom, right + default: + case 1: + case 3: + this.SetWindowPosition(WindowPositions.Right, Screen.PrimaryScreen); + Left -= Margin.Right; + break; + } + + Height = MinHeight = MaxHeight = (int)(Screen.PrimaryScreen.WpfBounds.Height - (2.0d * Margin.Top)); + Top = Margin.Top; + }); + } + + private void PowerManager_PowerStatusChanged(PowerStatus status) + { + // UI thread (async) + Application.Current.Dispatcher.Invoke(() => + { + var BatteryLifePercent = (int)Math.Truncate(status.BatteryLifePercent * 100.0f); + BatteryIndicatorPercentage.Text = $"{BatteryLifePercent}%"; + + // get status key + var KeyStatus = string.Empty; + switch (status.PowerLineStatus) + { + case PowerLineStatus.Online: + KeyStatus = "Charging"; + break; + default: + { + var energy = SystemPowerManager.EnergySaverStatus; + switch (energy) + { + case EnergySaverStatus.On: + KeyStatus = "Saver"; + break; + } + } + break; + } + + // get battery key + var KeyValue = (int)Math.Truncate(status.BatteryLifePercent * 10); + + // set key + var Key = $"Battery{KeyStatus}{KeyValue}"; + + if (SystemManager.PowerStatusIcon.TryGetValue(Key, out var glyph)) + BatteryIndicatorIcon.Glyph = glyph; + + if (status.BatteryLifeRemaining > 0) + { + var time = TimeSpan.FromSeconds(status.BatteryLifeRemaining); + + string remaining; + if (status.BatteryLifeRemaining >= 3600) + remaining = $"{time.Hours}h {time.Minutes}min"; + else + remaining = $"{time.Minutes}min"; + + BatteryIndicatorLifeRemaining.Text = $"({remaining} remaining)"; + BatteryIndicatorLifeRemaining.Visibility = Visibility.Visible; + } + else + { + BatteryIndicatorLifeRemaining.Text = string.Empty; + BatteryIndicatorLifeRemaining.Visibility = Visibility.Collapsed; + } + }); + } + + private void Window_Loaded(object sender, RoutedEventArgs e) + { + // load gamepad navigation maanger + gamepadFocusManager = new(this, ContentFrame); + + hwndSource = PresentationSource.FromVisual(this) as HwndSource; + hwndSource.AddHook(WndProc); + + if (hwndSource != null) + { + hwndSource.CompositionTarget.RenderMode = RenderMode.SoftwareOnly; + WinAPI.SetWindowPos(hwndSource.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE); + } + } + + private IntPtr prevWParam = new(0x0000000000000086); + private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + switch (msg) + { + case WM_SYSCOMMAND: + { + var command = wParam.ToInt32() & 0xfff0; + if (command == SC_MOVE) handled = true; + } + break; + + case WM_SETFOCUS: + { + if (hwndSource != null) + WinAPI.SetWindowPos(hwndSource.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE); + handled = true; + } + break; + + case WM_NCACTIVATE: + { + // prevent window from loosing its fancy style + if (wParam == 0 && (lParam == 0)) + { + if (prevWParam != new IntPtr(0x0000000000000086)) + if (AutoHide && Visibility == Visibility.Visible) + ToggleVisibility(); + handled = true; + } + + if (wParam == 1) + { + handled = true; + } + + prevWParam = wParam; + } + break; + + case WM_ACTIVATEAPP: + { + if (wParam == 0) + { + if (hwndSource != null) + WPFUtils.SendMessage(hwndSource.Handle, WM_NCACTIVATE, WM_NCACTIVATE, 0); + } + } + break; + + case WM_ACTIVATE: + { + // WA_INACTIVE + if (wParam == 0) + handled = true; + } + break; + + case WM_MOUSEACTIVATE: + { + handled = true; + } + break; + + case WM_PAINT: + { + // Loop through all visual elements in the window + foreach (var element in WPFUtils.FindVisualChildren(this)) + { + if (element.CacheMode is not null) + { + // Store the previous CacheMode value + cacheModes[element] = element.CacheMode.Clone(); + + // Set the CacheMode to null + element.CacheMode = null; + } + } + + WM_PAINT_TIMER.Stop(); + WM_PAINT_TIMER.Start(); + } + break; + } + + return IntPtr.Zero; + } + + private void WM_PAINT_TIMER_Tick(object? sender, EventArgs e) + { + // UI thread + Application.Current.Dispatcher.Invoke(() => + { + // Set the CacheMode back to the previous value + foreach (UIElement element in cacheModes.Keys) + element.CacheMode = cacheModes[element]; + }); + } + + private void HandleEsc(object sender, KeyEventArgs e) + { + if (e.Key == Key.Escape) + ToggleVisibility(); + } + + public void ToggleVisibility() + { + // UI thread + Application.Current.Dispatcher.Invoke(() => + { + switch (Visibility) + { + case Visibility.Collapsed: + case Visibility.Hidden: + Show(); + Focus(); + + if (hwndSource != null) + WPFUtils.SendMessage(hwndSource.Handle, WM_NCACTIVATE, WM_NCACTIVATE, 0); + + InvokeGotGamepadWindowFocus(); + + clockUpdateTimer.Start(); + break; + case Visibility.Visible: + Hide(); + + InvokeLostGamepadWindowFocus(); + + clockUpdateTimer.Stop(); + break; + } + }); + } + + private void Window_Closing(object sender, CancelEventArgs e) + { + e.Cancel = !isClosing; + + if (!isClosing) + ToggleVisibility(); + else + { + // close pages + devicePage.Close(); + } + } + + public void Close(bool v) + { + isClosing = v; + Close(); + } + + #region navView + + private void navView_ItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args) + { + if (args.InvokedItemContainer is not null) + { + var navItem = (NavigationViewItem)args.InvokedItemContainer; + var navItemTag = (string)navItem.Tag; + + switch (navItemTag) + { + default: + preNavItemTag = navItemTag; + break; + case "shortcutKeyboard": + case "shortcutDesktop": + case "shortcutESC": + case "shortcutExpand": + HotkeysManager.TriggerRaised(navItemTag, null, 0, false, true); + break; + } + + NavView_Navigate(preNavItemTag); + } + } + + public void NavView_Navigate(string navItemTag) + { + var item = _pages.FirstOrDefault(p => p.Key.Equals(navItemTag)); + var _page = item.Value; + + // Get the page type before navigation so you can prevent duplicate + // entries in the backstack. + var preNavPageType = ContentFrame.CurrentSourcePageType; + + // Only navigate if the selected page isn't currently loaded. + if (!(_page is null) && !Equals(preNavPageType, _page)) NavView_Navigate(_page); + } + + public void NavView_Navigate(Page _page) + { + ContentFrame.Navigate(_page); + } + + private void navView_Loaded(object sender, RoutedEventArgs e) + { + // Add handler for ContentFrame navigation. + ContentFrame.Navigated += On_Navigated; + + // If navigation occurs on SelectionChanged, this isn't needed. + // Because we use ItemInvoked to navigate, we need to call Navigate + // here to load the home page. + preNavItemTag = "QuickHomePage"; + NavView_Navigate(preNavItemTag); + } + + private void navView_BackRequested(NavigationView sender, NavigationViewBackRequestedEventArgs args) + { + TryGoBack(); + } + + private bool TryGoBack() + { + if (!ContentFrame.CanGoBack) + return false; + + // Don't go back if the nav pane is overlayed. + if (navView.IsPaneOpen && + (navView.DisplayMode == NavigationViewDisplayMode.Compact || + navView.DisplayMode == NavigationViewDisplayMode.Minimal)) + return false; + + ContentFrame.GoBack(); + return true; + } + + private void On_Navigated(object sender, NavigationEventArgs e) + { + navView.IsBackEnabled = ContentFrame.CanGoBack; + navHeader.Text = ((Page)((ContentControl)sender).Content).Title; + } + + private void UpdateTime(object? sender, EventArgs e) + { + var timeFormat = CultureInfo.InstalledUICulture.DateTimeFormat.ShortTimePattern; + Time.Text = DateTime.Now.ToString(timeFormat); + } + + #endregion } \ No newline at end of file diff --git a/HandheldCompanion/Views/Windows/OverlayTrackpad.xaml.cs b/HandheldCompanion/Views/Windows/OverlayTrackpad.xaml.cs index 206af74c5..1624a35c4 100644 --- a/HandheldCompanion/Views/Windows/OverlayTrackpad.xaml.cs +++ b/HandheldCompanion/Views/Windows/OverlayTrackpad.xaml.cs @@ -1,240 +1,240 @@ -using HandheldCompanion.Managers; -using HandheldCompanion.Views.Classes; -using System; -using System.ComponentModel; -using System.Windows; -using System.Windows.Forms; -using System.Windows.Input; -using static HandheldCompanion.DS4Touch; -using Application = System.Windows.Application; -using HorizontalAlignment = System.Windows.HorizontalAlignment; - -namespace HandheldCompanion.Views.Windows; - -/// -/// Interaction logic for Overlay.xaml -/// -public partial class OverlayTrackpad : OverlayWindow -{ - private readonly double dpiInput; - - private readonly TouchInput leftInput; - private readonly TouchInput rightInput; - - private double TrackpadOpacity = 0.25; - private readonly double TrackpadOpacityTouched = 0.10; // extra opacity when touched - - public OverlayTrackpad() - { - InitializeComponent(); - this._hotkeyId = 2; - - SettingsManager.SettingValueChanged += SettingsManager_SettingValueChanged; - - // touch vars - dpiInput = GetWindowsScaling(); - leftInput = new TouchInput(); - rightInput = new TouchInput(); - } - - private void SettingsManager_SettingValueChanged(string name, object value) - { - // UI thread - Application.Current.Dispatcher.Invoke(() => - { - switch (name) - { - case "OverlayTrackpadsSize": - { - var size = Convert.ToInt32(value); - LeftTrackpad.Width = size; - RightTrackpad.Width = size; - Height = size; - HorizontalAlignment = HorizontalAlignment.Stretch; - } - break; - case "OverlayTrackpadsAlignment": - { - var trackpadsAlignment = Convert.ToInt32(value); - switch (trackpadsAlignment) - { - case 0: - VerticalAlignment = VerticalAlignment.Top; - break; - case 1: - VerticalAlignment = VerticalAlignment.Center; - break; - case 2: - VerticalAlignment = VerticalAlignment.Bottom; - break; - } - } - break; - case "OverlayTrackpadsOpacity": - { - TrackpadOpacity = Convert.ToDouble(value); - LeftTrackpad.Opacity = TrackpadOpacity; - RightTrackpad.Opacity = TrackpadOpacity; - } - break; - } - }); - } - - private void Window_Closing(object sender, CancelEventArgs e) - { - // do something - } - - public double GetWindowsScaling() - { - return Screen.PrimaryScreen.Bounds.Width / SystemParameters.PrimaryScreenWidth; - } - - private void Trackpad_TouchInput(TouchEventArgs e, CursorAction action, CursorButton button) - { - var args = e.TouchDevice; - TouchPoint point; - int flags; - - switch (button) - { - default: - case CursorButton.TouchLeft: - { - point = args.GetTouchPoint(LeftTrackpad); - flags = leftInput.Flags; - - leftInput.Timestamp = e.Timestamp; - } - break; - case CursorButton.TouchRight: - { - point = args.GetTouchPoint(RightTrackpad); - flags = rightInput.Flags; - - rightInput.Timestamp = e.Timestamp; - } - break; - } - - var normalizedX = point.Position.X / LeftTrackpad.ActualWidth * dpiInput / 2.0d; - var normalizedY = point.Position.Y / LeftTrackpad.ActualWidth * dpiInput; - - normalizedX += button == CursorButton.TouchRight ? 0.5d : 0.0d; - - switch (action) - { - case CursorAction.CursorUp: - DS4Touch.OnMouseUp(normalizedX, normalizedY, button, flags); - break; - case CursorAction.CursorDown: - DS4Touch.OnMouseDown(normalizedX, normalizedY, button, flags); - break; - case CursorAction.CursorMove: - DS4Touch.OnMouseMove(normalizedX, normalizedY, button, flags); - break; - } - } - - private void Trackpad_PreviewTouchMove(object sender, TouchEventArgs e) - { - var name = ((FrameworkElement)sender).Name; - - switch (name) - { - case "LeftTrackpad": - { - Trackpad_TouchInput(e, CursorAction.CursorMove, CursorButton.TouchLeft); - } - break; - case "RightTrackpad": - { - Trackpad_TouchInput(e, CursorAction.CursorMove, CursorButton.TouchRight); - } - break; - } - - e.Handled = true; - } - - private void Trackpad_PreviewTouchDown(object sender, TouchEventArgs e) - { - var name = ((FrameworkElement)sender).Name; - - switch (name) - { - case "LeftTrackpad": - { - var elapsed = e.Timestamp - leftInput.Timestamp; - if (elapsed < SystemInformation.DoubleClickTime) - leftInput.Flags = 30; - - Trackpad_TouchInput(e, CursorAction.CursorDown, CursorButton.TouchLeft); - - LeftTrackpad.Opacity = TrackpadOpacity + TrackpadOpacityTouched; - - // send vibration (todo: make it a setting) - ControllerManager.GetTargetController()?.Rumble(); // (1, 25, 0, 60); - } - break; - case "RightTrackpad": - { - var elapsed = e.Timestamp - rightInput.Timestamp; - if (elapsed < SystemInformation.DoubleClickTime) - rightInput.Flags = 30; - - Trackpad_TouchInput(e, CursorAction.CursorDown, CursorButton.TouchRight); - - RightTrackpad.Opacity = TrackpadOpacity + TrackpadOpacityTouched; - - // send vibration (todo: make it a setting) - ControllerManager.GetTargetController()?.Rumble(); // (1, 25, 0, 60); - } - break; - } - - e.Handled = true; - } - - private void Trackpad_PreviewTouchUp(object sender, TouchEventArgs e) - { - var name = ((FrameworkElement)sender).Name; - - switch (name) - { - case "LeftTrackpad": - { - leftInput.Flags = 0; - Trackpad_TouchInput(e, CursorAction.CursorUp, CursorButton.TouchLeft); - LeftTrackpad.Opacity = TrackpadOpacity - TrackpadOpacityTouched; - } - break; - case "RightTrackpad": - { - rightInput.Flags = 0; - Trackpad_TouchInput(e, CursorAction.CursorUp, CursorButton.TouchRight); - RightTrackpad.Opacity = TrackpadOpacity - TrackpadOpacityTouched; - } - break; - } - - e.Handled = true; - } - - private class TouchInput - { - public short Flags; - public int Timestamp; - } - - private void LeftTrackpadClick_PreviewTouchDown(object sender, TouchEventArgs e) - { - DS4Touch.OutputClickButton = true; - } - - private void RightTrackpadClick_PreviewTouchDown(object sender, TouchEventArgs e) - { - DS4Touch.OutputClickButton = true; - } +using HandheldCompanion.Managers; +using HandheldCompanion.Views.Classes; +using System; +using System.ComponentModel; +using System.Windows; +using System.Windows.Forms; +using System.Windows.Input; +using static HandheldCompanion.DS4Touch; +using Application = System.Windows.Application; +using HorizontalAlignment = System.Windows.HorizontalAlignment; + +namespace HandheldCompanion.Views.Windows; + +/// +/// Interaction logic for Overlay.xaml +/// +public partial class OverlayTrackpad : OverlayWindow +{ + private readonly double dpiInput; + + private readonly TouchInput leftInput; + private readonly TouchInput rightInput; + + private double TrackpadOpacity = 0.25; + private readonly double TrackpadOpacityTouched = 0.10; // extra opacity when touched + + public OverlayTrackpad() + { + InitializeComponent(); + this._hotkeyId = 2; + + SettingsManager.SettingValueChanged += SettingsManager_SettingValueChanged; + + // touch vars + dpiInput = GetWindowsScaling(); + leftInput = new TouchInput(); + rightInput = new TouchInput(); + } + + private void SettingsManager_SettingValueChanged(string name, object value) + { + // UI thread + Application.Current.Dispatcher.Invoke(() => + { + switch (name) + { + case "OverlayTrackpadsSize": + { + var size = Convert.ToInt32(value); + LeftTrackpad.Width = size; + RightTrackpad.Width = size; + Height = size; + HorizontalAlignment = HorizontalAlignment.Stretch; + } + break; + case "OverlayTrackpadsAlignment": + { + var trackpadsAlignment = Convert.ToInt32(value); + switch (trackpadsAlignment) + { + case 0: + VerticalAlignment = VerticalAlignment.Top; + break; + case 1: + VerticalAlignment = VerticalAlignment.Center; + break; + case 2: + VerticalAlignment = VerticalAlignment.Bottom; + break; + } + } + break; + case "OverlayTrackpadsOpacity": + { + TrackpadOpacity = Convert.ToDouble(value); + LeftTrackpad.Opacity = TrackpadOpacity; + RightTrackpad.Opacity = TrackpadOpacity; + } + break; + } + }); + } + + private void Window_Closing(object sender, CancelEventArgs e) + { + // do something + } + + public double GetWindowsScaling() + { + return Screen.PrimaryScreen.Bounds.Width / SystemParameters.PrimaryScreenWidth; + } + + private void Trackpad_TouchInput(TouchEventArgs e, CursorAction action, CursorButton button) + { + var args = e.TouchDevice; + TouchPoint point; + int flags; + + switch (button) + { + default: + case CursorButton.TouchLeft: + { + point = args.GetTouchPoint(LeftTrackpad); + flags = leftInput.Flags; + + leftInput.Timestamp = e.Timestamp; + } + break; + case CursorButton.TouchRight: + { + point = args.GetTouchPoint(RightTrackpad); + flags = rightInput.Flags; + + rightInput.Timestamp = e.Timestamp; + } + break; + } + + var normalizedX = point.Position.X / LeftTrackpad.ActualWidth * dpiInput / 2.0d; + var normalizedY = point.Position.Y / LeftTrackpad.ActualWidth * dpiInput; + + normalizedX += button == CursorButton.TouchRight ? 0.5d : 0.0d; + + switch (action) + { + case CursorAction.CursorUp: + DS4Touch.OnMouseUp(normalizedX, normalizedY, button, flags); + break; + case CursorAction.CursorDown: + DS4Touch.OnMouseDown(normalizedX, normalizedY, button, flags); + break; + case CursorAction.CursorMove: + DS4Touch.OnMouseMove(normalizedX, normalizedY, button, flags); + break; + } + } + + private void Trackpad_PreviewTouchMove(object sender, TouchEventArgs e) + { + var name = ((FrameworkElement)sender).Name; + + switch (name) + { + case "LeftTrackpad": + { + Trackpad_TouchInput(e, CursorAction.CursorMove, CursorButton.TouchLeft); + } + break; + case "RightTrackpad": + { + Trackpad_TouchInput(e, CursorAction.CursorMove, CursorButton.TouchRight); + } + break; + } + + e.Handled = true; + } + + private void Trackpad_PreviewTouchDown(object sender, TouchEventArgs e) + { + var name = ((FrameworkElement)sender).Name; + + switch (name) + { + case "LeftTrackpad": + { + var elapsed = e.Timestamp - leftInput.Timestamp; + if (elapsed < SystemInformation.DoubleClickTime) + leftInput.Flags = 30; + + Trackpad_TouchInput(e, CursorAction.CursorDown, CursorButton.TouchLeft); + + LeftTrackpad.Opacity = TrackpadOpacity + TrackpadOpacityTouched; + + // send vibration (todo: make it a setting) + ControllerManager.GetTargetController()?.Rumble(); // (1, 25, 0, 60); + } + break; + case "RightTrackpad": + { + var elapsed = e.Timestamp - rightInput.Timestamp; + if (elapsed < SystemInformation.DoubleClickTime) + rightInput.Flags = 30; + + Trackpad_TouchInput(e, CursorAction.CursorDown, CursorButton.TouchRight); + + RightTrackpad.Opacity = TrackpadOpacity + TrackpadOpacityTouched; + + // send vibration (todo: make it a setting) + ControllerManager.GetTargetController()?.Rumble(); // (1, 25, 0, 60); + } + break; + } + + e.Handled = true; + } + + private void Trackpad_PreviewTouchUp(object sender, TouchEventArgs e) + { + var name = ((FrameworkElement)sender).Name; + + switch (name) + { + case "LeftTrackpad": + { + leftInput.Flags = 0; + Trackpad_TouchInput(e, CursorAction.CursorUp, CursorButton.TouchLeft); + LeftTrackpad.Opacity = TrackpadOpacity - TrackpadOpacityTouched; + } + break; + case "RightTrackpad": + { + rightInput.Flags = 0; + Trackpad_TouchInput(e, CursorAction.CursorUp, CursorButton.TouchRight); + RightTrackpad.Opacity = TrackpadOpacity - TrackpadOpacityTouched; + } + break; + } + + e.Handled = true; + } + + private class TouchInput + { + public short Flags; + public int Timestamp; + } + + private void LeftTrackpadClick_PreviewTouchDown(object sender, TouchEventArgs e) + { + DS4Touch.OutputClickButton = true; + } + + private void RightTrackpadClick_PreviewTouchDown(object sender, TouchEventArgs e) + { + DS4Touch.OutputClickButton = true; + } } \ No newline at end of file diff --git a/HandheldCompanion/WMI.cs b/HandheldCompanion/WMI.cs index 72f784be1..af49c8525 100644 --- a/HandheldCompanion/WMI.cs +++ b/HandheldCompanion/WMI.cs @@ -1,126 +1,126 @@ -using HandheldCompanion.Extensions; -using HandheldCompanion.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Management; -using System.Threading.Tasks; - -namespace HandheldCompanion -{ - public static class WMI - { - public static async Task ExistsAsync(string scope, FormattableString query) - { - try - { - var queryFormatted = query.ToString(WMIPropertyValueFormatter.Instance); - var mos = new ManagementObjectSearcher(scope, queryFormatted); - var managementObjects = await mos.GetAsync().ConfigureAwait(false); - return managementObjects.Any(); - } - catch - { - return false; - } - } - - public static IDisposable Listen(string scope, FormattableString query, Action handler) - { - var queryFormatted = query.ToString(WMIPropertyValueFormatter.Instance); - var watcher = new ManagementEventWatcher(scope, queryFormatted); - watcher.EventArrived += (_, e) => handler(e.NewEvent.Properties); - watcher.Start(); - - return new LambdaDisposable(() => - { - watcher.Stop(); - watcher.Dispose(); - }); - } - - public static async Task> ReadAsync(string scope, FormattableString query, Func converter) - { - try - { - var queryFormatted = query.ToString(WMIPropertyValueFormatter.Instance); - var mos = new ManagementObjectSearcher(scope, queryFormatted); - var managementObjects = await mos.GetAsync().ConfigureAwait(false); - var result = managementObjects.Select(mo => mo.Properties).Select(converter); - return result; - } - catch (ManagementException ex) - { - throw new ManagementException($"Read failed: {ex.Message}. [scope={scope}, query={query}]", ex); - } - } - - public static async Task CallAsync(string scope, FormattableString query, string methodName, Dictionary methodParams) - { - try - { - var queryFormatted = query.ToString(WMIPropertyValueFormatter.Instance); - var mos = new ManagementObjectSearcher(scope, queryFormatted); - var managementObjects = await mos.GetAsync().ConfigureAwait(false); - var managementObject = managementObjects.FirstOrDefault() ?? throw new InvalidOperationException("No results in query"); - - var mo = (ManagementObject)managementObject; - var methodParamsObject = mo.GetMethodParameters(methodName); - foreach (var pair in methodParams) - methodParamsObject[pair.Key] = pair.Value; - - mo.InvokeMethod(methodName, methodParamsObject, new InvokeMethodOptions()); - } - catch (ManagementException ex) - { - throw new ManagementException($"Call failed: {ex.Message}. [scope={scope}, query={query}, methodName={methodName}]", ex); - } - } - - public static async Task CallAsync(string scope, FormattableString query, string methodName, Dictionary methodParams, Func converter) - { - try - { - var queryFormatted = query.ToString(WMIPropertyValueFormatter.Instance); - - var mos = new ManagementObjectSearcher(scope, queryFormatted); - var managementObjects = await mos.GetAsync().ConfigureAwait(false); - var managementObject = managementObjects.FirstOrDefault() ?? throw new InvalidOperationException("No results in query"); - - var mo = (ManagementObject)managementObject; - var methodParamsObject = mo.GetMethodParameters(methodName); - foreach (var pair in methodParams) - methodParamsObject[pair.Key] = pair.Value; - - var resultProperties = mo.InvokeMethod(methodName, methodParamsObject, new InvokeMethodOptions()); - var result = converter(resultProperties.Properties); - return result; - } - catch (ManagementException ex) - { - throw new ManagementException($"Call failed: {ex.Message}. [scope={scope}, query={query}, methodName={methodName}]", ex); - } - } - - public class WMIPropertyValueFormatter : IFormatProvider, ICustomFormatter - { - public static readonly WMIPropertyValueFormatter Instance = new(); - - private WMIPropertyValueFormatter() { } - - public object GetFormat(Type? formatType) - { - if (formatType == typeof(ICustomFormatter)) - return this; - - throw new InvalidOperationException("Invalid type of formatted"); - } - - public string Format(string? format, object? arg, IFormatProvider? formatProvider) - { - var stringArg = arg?.ToString()?.Replace("\\", "\\\\"); - return stringArg ?? string.Empty; - } - } - } -} +using HandheldCompanion.Extensions; +using HandheldCompanion.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management; +using System.Threading.Tasks; + +namespace HandheldCompanion +{ + public static class WMI + { + public static async Task ExistsAsync(string scope, FormattableString query) + { + try + { + var queryFormatted = query.ToString(WMIPropertyValueFormatter.Instance); + var mos = new ManagementObjectSearcher(scope, queryFormatted); + var managementObjects = await mos.GetAsync().ConfigureAwait(false); + return managementObjects.Any(); + } + catch + { + return false; + } + } + + public static IDisposable Listen(string scope, FormattableString query, Action handler) + { + var queryFormatted = query.ToString(WMIPropertyValueFormatter.Instance); + var watcher = new ManagementEventWatcher(scope, queryFormatted); + watcher.EventArrived += (_, e) => handler(e.NewEvent.Properties); + watcher.Start(); + + return new LambdaDisposable(() => + { + watcher.Stop(); + watcher.Dispose(); + }); + } + + public static async Task> ReadAsync(string scope, FormattableString query, Func converter) + { + try + { + var queryFormatted = query.ToString(WMIPropertyValueFormatter.Instance); + var mos = new ManagementObjectSearcher(scope, queryFormatted); + var managementObjects = await mos.GetAsync().ConfigureAwait(false); + var result = managementObjects.Select(mo => mo.Properties).Select(converter); + return result; + } + catch (ManagementException ex) + { + throw new ManagementException($"Read failed: {ex.Message}. [scope={scope}, query={query}]", ex); + } + } + + public static async Task CallAsync(string scope, FormattableString query, string methodName, Dictionary methodParams) + { + try + { + var queryFormatted = query.ToString(WMIPropertyValueFormatter.Instance); + var mos = new ManagementObjectSearcher(scope, queryFormatted); + var managementObjects = await mos.GetAsync().ConfigureAwait(false); + var managementObject = managementObjects.FirstOrDefault() ?? throw new InvalidOperationException("No results in query"); + + var mo = (ManagementObject)managementObject; + var methodParamsObject = mo.GetMethodParameters(methodName); + foreach (var pair in methodParams) + methodParamsObject[pair.Key] = pair.Value; + + mo.InvokeMethod(methodName, methodParamsObject, new InvokeMethodOptions()); + } + catch (ManagementException ex) + { + throw new ManagementException($"Call failed: {ex.Message}. [scope={scope}, query={query}, methodName={methodName}]", ex); + } + } + + public static async Task CallAsync(string scope, FormattableString query, string methodName, Dictionary methodParams, Func converter) + { + try + { + var queryFormatted = query.ToString(WMIPropertyValueFormatter.Instance); + + var mos = new ManagementObjectSearcher(scope, queryFormatted); + var managementObjects = await mos.GetAsync().ConfigureAwait(false); + var managementObject = managementObjects.FirstOrDefault() ?? throw new InvalidOperationException("No results in query"); + + var mo = (ManagementObject)managementObject; + var methodParamsObject = mo.GetMethodParameters(methodName); + foreach (var pair in methodParams) + methodParamsObject[pair.Key] = pair.Value; + + var resultProperties = mo.InvokeMethod(methodName, methodParamsObject, new InvokeMethodOptions()); + var result = converter(resultProperties.Properties); + return result; + } + catch (ManagementException ex) + { + throw new ManagementException($"Call failed: {ex.Message}. [scope={scope}, query={query}, methodName={methodName}]", ex); + } + } + + public class WMIPropertyValueFormatter : IFormatProvider, ICustomFormatter + { + public static readonly WMIPropertyValueFormatter Instance = new(); + + private WMIPropertyValueFormatter() { } + + public object GetFormat(Type? formatType) + { + if (formatType == typeof(ICustomFormatter)) + return this; + + throw new InvalidOperationException("Invalid type of formatted"); + } + + public string Format(string? format, object? arg, IFormatProvider? formatProvider) + { + var stringArg = arg?.ToString()?.Replace("\\", "\\\\"); + return stringArg ?? string.Empty; + } + } + } +} diff --git a/README.md b/README.md index 67469e167..db7a2b50b 100644 --- a/README.md +++ b/README.md @@ -1,147 +1,147 @@ -[![Download Latest](https://img.shields.io/github/downloads/Valkirie/HandheldCompanion/latest/total?style=flat-square&color=orange&label=Download%20Latest)](https://github.com/Valkirie/HandheldCompanion/releases/latest) -[![discord](https://img.shields.io/discord/1054321983166365726?color=orange&label=Discord&logo=discord&logoColor=white&style=flat-square)](https://discord.gg/znHuywFz5M) -[![YouTube Channel](https://img.shields.io/youtube/channel/subscribers/UCFLra6QVYJYeaWp2mGaq3Og?style=flat-square&color=orange&label=YouTube%20Channel&logo=youtube&logoColor=white)](https://www.youtube.com/channel/UCFLra6QVYJYeaWp2mGaq3Og) -[![Donations](https://img.shields.io/badge/PayPal-00457C?style=flat-square&color=orange&label=Donations&logo=paypal&logoColor=white)](https://www.paypal.com/paypalme/BenjaminLSR) -[![Support me on Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dhandheldcompanion%26type%3Dpatrons&style=flat-square&color=orange&label=Patreon&logoColor=white)](https://patreon.com/handheldcompanion) - -# Handheld Companion - -A touch optimized GUI to increase your handheld gaming computer experience. Features include: -- Motion control a.k.a. gyro control through a device's inertial measurement unit (IMU, Gyroscope and Accelerometer) or external sensor. Settings availible for racing, 1st and 3rd person gaming and emulator support. -- Quicktools overlay, with easy access to various settings and informatio such as TDP, Auto TDP, GPU, Screen Hz, Framelimiter, Resolution, Brightness, Volume, Powermode control and battery level. -- Virtual controller simulation of [Microsoft Xbox 360 Controller](https://en.wikipedia.org/wiki/Xbox_360_controller) and [Sony DualShock 4 Controller](https://en.wikipedia.org/wiki/DualShock#DualShock_4). -- Profile settings system, automatic detection of active game and applying of settings. -- Gamepad remapping to mouse and keyboard, gamepad joystick and trigger deadzone adjustements. -- PS Remote Play support with DS4 controller, including motion and touchpad. -- 3D Controller overlay for stream recordings. -- Hotkeys for various conveniences. - -## Use cases -A few examples of the most common use cases are: -- You want to add universal motion controls (UMC) to any game. -- You want to add high-precision motion controls to your Windows game library through [Steam](https://store.steampowered.com/controller/update/dec15). -- You want to play your Sony Playstation 4 library through [PlayStation Now](https://www.playstation.com/en-us/ps-now/) or [PS4 Remote Play](). -- You want to enjoy all your [Wii](https://dolphin-emu.org/), [WiiU](https://cemu.info/) and [Switch](https://yuzu-emu.org/) games with full motion controls through UDP motion control protocol. - -[Youtube Channel](https://www.youtube.com/channel/UCFLra6QVYJYeaWp2mGaq3Og) - -## Supported Systems -The software is built for Windows 10/Windows 11 (x86 and amd64). - -## Supported Devices - -- ASUS ROG Ally -- AOKZOE A1 -- AYA Neo and its different versions -- AYA Neo Next and its different versions -- AYA Neo Air and it's different versions -- AYA Neo 2, Geek, 2S Geek 1S -- AYA Neo KUN -- ONEXPLAYER MINI and its different versions (Intel, AMD, Gundam) -- GPD WIN Max 2 (INTEL, AMD) -- GPD Win 2 -- GPD Win 3 -- GPD Win 4 -- Steam Deck (LCD, OLED) -- Ayn Loki (all models) -- Lenovo Legion Go - -## Supported Sensors -- Bosch BMI160 (and similar) -- USB IMU (GY-USB002) - -## Supported Languages -- English -- French -- German -- Italian -- Japanese -- Portuguese (Brazilian) -- Spanish -- Chinese (Simplified) -- Chinese (Traditional) - -## Partners -![image](https://github.com/Valkirie/HandheldCompanion/assets/934757/0833e620-c629-4f63-b337-9b10138988b7) - -[DroiX](https://droix.net/?ref=dxhc), are trusted and respected sellers for PC gaming handhelds including GPD, AYA NEO, ONEXPLAYER & AOKZOE. Shipping worldwide with local return centers, their expert customer service is there to provide support, answer any queries and ensure you get the best support for your purchase. Try them today! - -[Droix Discord](https://go.droix.co.uk/discord) - -## Visuals -![image](https://github.com/Valkirie/HandheldCompanion/assets/14330834/c6aec83c-cf8e-43bf-a577-1b05aedc55aa) -![image](https://github.com/Valkirie/HandheldCompanion/assets/14330834/dba3ab8a-4b73-4e61-a278-7fe37c66b85d) -![image](https://github.com/Valkirie/HandheldCompanion/assets/14330834/0bacdd65-3d6d-4dd2-a70d-a6871b1f51a9) - -## Overlay -The software has multiple built-in performance metric overlay options. - -![image](https://github.com/Valkirie/HandheldCompanion/assets/14330834/b01e757d-3a68-4d8d-b62e-9129b577d022) - -### QuickTools - -On the fly adjustment of TDP (global and profile), brightness, screen resolution and frequency, hotkeys and motion control profile settings. Summonable with a user defined button combination (including certaind supported devices mapped special keys). Window can be aligned how the user sees fit (left, right, floating). - -![image](https://github.com/Valkirie/HandheldCompanion/assets/14330834/889a1a1c-4775-4261-a173-c275eb4071ad) -Quicktools power control with Into the Breach. - -![image](https://github.com/Valkirie/HandheldCompanion/assets/14330834/ec35272e-4c9e-4386-9b0f-3b4e3aa0cf6d) -Quicktools profile settings with Borderlands Pre-Sequel. - -### Virtual touchpad - -Virtual touchpad on top of your gaming sessions. The virtual touchpad is used to mimic the DualShock 4 physical touchpad and grants maximum compatibility with PS Now, PS Remote software suites and games that make specific use of the Steampad touchpads. - -![Touchpad](https://thumbs.gfycat.com/DiscreteJollyBluemorphobutterfly-size_restricted.gif) - -Virtual Touchpad input demonstration with [PS Remote Play](https://remoteplay.dl.playstation.net/remoteplay/lang/en/) - -![Example02](https://user-images.githubusercontent.com/14330834/184550793-d81e2ec9-0271-4aae-bc44-7aeb393631ea.png) - -PS Remote Play, The Last of Us Part 2 - -### 3D Controller - -Display a 3D virtual controller, showcasing the motion of the device and all button interaction, individual button presses, joystick and trigger positions. The following 3D models are availible. - - OEM controller (Ayaneo Pro, Ayaneo Next, OneXPlayer Mini) - - Emulated controller (DualShock 4, Xbox 360) - - Xbox One controller - - ZDO+ controller - - Fisher-Price controller - - Machenike HG510 - - 8BitDo Lite 2 - - Nintendo 64 - - Dual Sense - -![image](https://thumbs.gfycat.com/BlackandwhiteRareBorderterrier-size_restricted.gif) - -## Contribute -### Bugs & Features -Found a bug and want it fixed? Open a detailed issue on the [GitHub issue tracker](../../issues)! -Have an idea for a new feature? Let's have a chat about your request on [Discord](https://discord.gg/znHuywFz5M). - -### Questions & Support -Please respect that the GitHub issue tracker isn't a helpdesk. We offer a [Discord server](https://discord.gg/znHuywFz5M), where you're welcome to check out and engage in discussions! - -### Donation -If you would like to support this project, please consider making a donation to `BenjaminLSR` via [PayPal](https://www.paypal.com/paypalme/BenjaminLSR). - -Handheld Companion relies on `ViGEmBus` driver and `ViGEmClient` libraries as well as `HidHide` kernel-mode filter driver. Therefore, we strongly encourage you in donating to `Nefarius` via [PayPal](https://paypal.me/NefariusMaximus) for continued maintenance and development. - -## Installation -Installers are [available as an all-in-one setup](../../releases/latest). -Run the `install.exe` as administrator and you'll be set! - -## Credits & Libraries -- ViGEmBus: [Nefarius](https://github.com/ViGEm/ViGEmBus) -- ViGEmClient : [Nefarius](https://github.com/ViGEm/ViGEmClient) -- SharpDX : [https://github.com/sharpdx/SharpDX](https://github.com/sharpdx/SharpDX) -- Godot Engine Illustration : [Juan Linietsky, Fernando Miguel Calabró](https://github.com/godotengine/tps-demo) - -## Licensing - -![image](https://user-images.githubusercontent.com/934757/159507299-ee55ec0b-8c0a-41b6-8dab-a1c72589565e.png)![image](https://user-images.githubusercontent.com/934757/159507349-caf88e3f-508b-4293-ae69-9918d6ba3d75.png)![image](https://user-images.githubusercontent.com/934757/159507749-c6ce02f6-b428-4592-96ca-95084ac5669b.png)![image](https://user-images.githubusercontent.com/934757/159507875-9ee29e9d-9528-4345-9503-0e2a13faeb4c.png) - -This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. - -We believe in the fair use of open-source solutions. We expect OEMs to come forward before distributing our solution with their devices. This way we can work together to make your device and our solution compatible in the best possible way. We reserve the right to take any action necessary to block partial or full access to the application to any entities that do not comply with the license or fair use principle. +[![Download Latest](https://img.shields.io/github/downloads/Valkirie/HandheldCompanion/latest/total?style=flat-square&color=orange&label=Download%20Latest)](https://github.com/Valkirie/HandheldCompanion/releases/latest) +[![discord](https://img.shields.io/discord/1054321983166365726?color=orange&label=Discord&logo=discord&logoColor=white&style=flat-square)](https://discord.gg/znHuywFz5M) +[![YouTube Channel](https://img.shields.io/youtube/channel/subscribers/UCFLra6QVYJYeaWp2mGaq3Og?style=flat-square&color=orange&label=YouTube%20Channel&logo=youtube&logoColor=white)](https://www.youtube.com/channel/UCFLra6QVYJYeaWp2mGaq3Og) +[![Donations](https://img.shields.io/badge/PayPal-00457C?style=flat-square&color=orange&label=Donations&logo=paypal&logoColor=white)](https://www.paypal.com/paypalme/BenjaminLSR) +[![Support me on Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dhandheldcompanion%26type%3Dpatrons&style=flat-square&color=orange&label=Patreon&logoColor=white)](https://patreon.com/handheldcompanion) + +# Handheld Companion + +A touch optimized GUI to increase your handheld gaming computer experience. Features include: +- Motion control a.k.a. gyro control through a device's inertial measurement unit (IMU, Gyroscope and Accelerometer) or external sensor. Settings availible for racing, 1st and 3rd person gaming and emulator support. +- Quicktools overlay, with easy access to various settings and informatio such as TDP, Auto TDP, GPU, Screen Hz, Framelimiter, Resolution, Brightness, Volume, Powermode control and battery level. +- Virtual controller simulation of [Microsoft Xbox 360 Controller](https://en.wikipedia.org/wiki/Xbox_360_controller) and [Sony DualShock 4 Controller](https://en.wikipedia.org/wiki/DualShock#DualShock_4). +- Profile settings system, automatic detection of active game and applying of settings. +- Gamepad remapping to mouse and keyboard, gamepad joystick and trigger deadzone adjustements. +- PS Remote Play support with DS4 controller, including motion and touchpad. +- 3D Controller overlay for stream recordings. +- Hotkeys for various conveniences. + +## Use cases +A few examples of the most common use cases are: +- You want to add universal motion controls (UMC) to any game. +- You want to add high-precision motion controls to your Windows game library through [Steam](https://store.steampowered.com/controller/update/dec15). +- You want to play your Sony Playstation 4 library through [PlayStation Now](https://www.playstation.com/en-us/ps-now/) or [PS4 Remote Play](). +- You want to enjoy all your [Wii](https://dolphin-emu.org/), [WiiU](https://cemu.info/) and [Switch](https://yuzu-emu.org/) games with full motion controls through UDP motion control protocol. + +[Youtube Channel](https://www.youtube.com/channel/UCFLra6QVYJYeaWp2mGaq3Og) + +## Supported Systems +The software is built for Windows 10/Windows 11 (x86 and amd64). + +## Supported Devices + +- ASUS ROG Ally +- AOKZOE A1 +- AYA Neo and its different versions +- AYA Neo Next and its different versions +- AYA Neo Air and it's different versions +- AYA Neo 2, Geek, 2S Geek 1S +- AYA Neo KUN +- ONEXPLAYER MINI and its different versions (Intel, AMD, Gundam) +- GPD WIN Max 2 (INTEL, AMD) +- GPD Win 2 +- GPD Win 3 +- GPD Win 4 +- Steam Deck (LCD, OLED) +- Ayn Loki (all models) +- Lenovo Legion Go + +## Supported Sensors +- Bosch BMI160 (and similar) +- USB IMU (GY-USB002) + +## Supported Languages +- English +- French +- German +- Italian +- Japanese +- Portuguese (Brazilian) +- Spanish +- Chinese (Simplified) +- Chinese (Traditional) + +## Partners +![image](https://github.com/Valkirie/HandheldCompanion/assets/934757/0833e620-c629-4f63-b337-9b10138988b7) + +[DroiX](https://droix.net/?ref=dxhc), are trusted and respected sellers for PC gaming handhelds including GPD, AYA NEO, ONEXPLAYER & AOKZOE. Shipping worldwide with local return centers, their expert customer service is there to provide support, answer any queries and ensure you get the best support for your purchase. Try them today! + +[Droix Discord](https://go.droix.co.uk/discord) + +## Visuals +![image](https://github.com/Valkirie/HandheldCompanion/assets/14330834/c6aec83c-cf8e-43bf-a577-1b05aedc55aa) +![image](https://github.com/Valkirie/HandheldCompanion/assets/14330834/dba3ab8a-4b73-4e61-a278-7fe37c66b85d) +![image](https://github.com/Valkirie/HandheldCompanion/assets/14330834/0bacdd65-3d6d-4dd2-a70d-a6871b1f51a9) + +## Overlay +The software has multiple built-in performance metric overlay options. + +![image](https://github.com/Valkirie/HandheldCompanion/assets/14330834/b01e757d-3a68-4d8d-b62e-9129b577d022) + +### QuickTools + +On the fly adjustment of TDP (global and profile), brightness, screen resolution and frequency, hotkeys and motion control profile settings. Summonable with a user defined button combination (including certaind supported devices mapped special keys). Window can be aligned how the user sees fit (left, right, floating). + +![image](https://github.com/Valkirie/HandheldCompanion/assets/14330834/889a1a1c-4775-4261-a173-c275eb4071ad) +Quicktools power control with Into the Breach. + +![image](https://github.com/Valkirie/HandheldCompanion/assets/14330834/ec35272e-4c9e-4386-9b0f-3b4e3aa0cf6d) +Quicktools profile settings with Borderlands Pre-Sequel. + +### Virtual touchpad + +Virtual touchpad on top of your gaming sessions. The virtual touchpad is used to mimic the DualShock 4 physical touchpad and grants maximum compatibility with PS Now, PS Remote software suites and games that make specific use of the Steampad touchpads. + +![Touchpad](https://thumbs.gfycat.com/DiscreteJollyBluemorphobutterfly-size_restricted.gif) + +Virtual Touchpad input demonstration with [PS Remote Play](https://remoteplay.dl.playstation.net/remoteplay/lang/en/) + +![Example02](https://user-images.githubusercontent.com/14330834/184550793-d81e2ec9-0271-4aae-bc44-7aeb393631ea.png) + +PS Remote Play, The Last of Us Part 2 + +### 3D Controller + +Display a 3D virtual controller, showcasing the motion of the device and all button interaction, individual button presses, joystick and trigger positions. The following 3D models are availible. + - OEM controller (Ayaneo Pro, Ayaneo Next, OneXPlayer Mini) + - Emulated controller (DualShock 4, Xbox 360) + - Xbox One controller + - ZDO+ controller + - Fisher-Price controller + - Machenike HG510 + - 8BitDo Lite 2 + - Nintendo 64 + - Dual Sense + +![image](https://thumbs.gfycat.com/BlackandwhiteRareBorderterrier-size_restricted.gif) + +## Contribute +### Bugs & Features +Found a bug and want it fixed? Open a detailed issue on the [GitHub issue tracker](../../issues)! +Have an idea for a new feature? Let's have a chat about your request on [Discord](https://discord.gg/znHuywFz5M). + +### Questions & Support +Please respect that the GitHub issue tracker isn't a helpdesk. We offer a [Discord server](https://discord.gg/znHuywFz5M), where you're welcome to check out and engage in discussions! + +### Donation +If you would like to support this project, please consider making a donation to `BenjaminLSR` via [PayPal](https://www.paypal.com/paypalme/BenjaminLSR). + +Handheld Companion relies on `ViGEmBus` driver and `ViGEmClient` libraries as well as `HidHide` kernel-mode filter driver. Therefore, we strongly encourage you in donating to `Nefarius` via [PayPal](https://paypal.me/NefariusMaximus) for continued maintenance and development. + +## Installation +Installers are [available as an all-in-one setup](../../releases/latest). +Run the `install.exe` as administrator and you'll be set! + +## Credits & Libraries +- ViGEmBus: [Nefarius](https://github.com/ViGEm/ViGEmBus) +- ViGEmClient : [Nefarius](https://github.com/ViGEm/ViGEmClient) +- SharpDX : [https://github.com/sharpdx/SharpDX](https://github.com/sharpdx/SharpDX) +- Godot Engine Illustration : [Juan Linietsky, Fernando Miguel Calabró](https://github.com/godotengine/tps-demo) + +## Licensing + +![image](https://user-images.githubusercontent.com/934757/159507299-ee55ec0b-8c0a-41b6-8dab-a1c72589565e.png)![image](https://user-images.githubusercontent.com/934757/159507349-caf88e3f-508b-4293-ae69-9918d6ba3d75.png)![image](https://user-images.githubusercontent.com/934757/159507749-c6ce02f6-b428-4592-96ca-95084ac5669b.png)![image](https://user-images.githubusercontent.com/934757/159507875-9ee29e9d-9528-4345-9503-0e2a13faeb4c.png) + +This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + +We believe in the fair use of open-source solutions. We expect OEMs to come forward before distributing our solution with their devices. This way we can work together to make your device and our solution compatible in the best possible way. We reserve the right to take any action necessary to block partial or full access to the application to any entities that do not comply with the license or fair use principle. diff --git a/hidapi.net/HidDevice.cs b/hidapi.net/HidDevice.cs index 6aa658e6d..75ce778e1 100644 --- a/hidapi.net/HidDevice.cs +++ b/hidapi.net/HidDevice.cs @@ -1,179 +1,179 @@ -using hidapi.Native; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace hidapi -{ - public class HidDevice : IDisposable - { - private ushort _vid, _pid, _inputBufferLen; - private short _mi; - private IntPtr _deviceHandle; - private object _lock = new object(); - private bool _reading = false; - private Thread _readThread; - private long MillisecondsSinceEpoch => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - public bool IsDeviceValid => _deviceHandle != IntPtr.Zero; - public bool Reading => _reading; - public Func OnInputReceived; - - public HidDevice(ushort vendorId, ushort productId, ushort inputBufferLen = 64, short mi = -1) - { - _vid = vendorId; - _pid = productId; - _inputBufferLen = inputBufferLen; - _mi = mi; - } - - private void ThrowIfDeviceInvalid() - { - if (!IsDeviceValid) - throw new HidDeviceInvalidException(); - } - - private static short GetMI(string path) - { - string low = path.ToLower(); - int index = low.IndexOf("mi_"); - if (index == -1) - return -1; - string mi = low.Substring(index + 3, 2); - - if (short.TryParse(mi, out short number)) - return number; - - return -1; - } - - public Task OpenDeviceAsync() => Task.Run(() => OpenDevice()); - public bool OpenDevice() - { - lock (_lock) - { - IntPtr devEnum = HidApiNative.hid_enumerate(_vid, _pid); - IntPtr deviceInfo = devEnum; - - while (deviceInfo != IntPtr.Zero) - { - HidDeviceInfo hidDeviceInfo = new HidDeviceInfo(deviceInfo); - if (_mi != -1 && _mi != GetMI(hidDeviceInfo.Path)) - goto next; - - _deviceHandle = HidApiNative.hid_open_path(hidDeviceInfo.Path); - if (_deviceHandle != IntPtr.Zero) - break; - - next: - deviceInfo = hidDeviceInfo.NextDevicePtr; - } - - HidApiNative.hid_free_enumeration(deviceInfo); - return _deviceHandle != IntPtr.Zero; - } - } - - public Task ReadAsync(int timeout = 100) => Task.Run(() => Read(timeout)); - public byte[] Read(int timeout = 100) - { - ThrowIfDeviceInvalid(); - lock (_lock) - { - byte[] buffer = new byte[_inputBufferLen]; - int length = HidApiNative.hid_read_timeout(_deviceHandle, buffer, (uint)buffer.Length, timeout); - return buffer; - } - } - - public Task ReadAsync(byte[] data) => Task.Run(() => Read(data)); - public int Read(byte[] buffer, int timeout = 100) - { - if (buffer.Length < _inputBufferLen) - throw new ArgumentException("Buffer length is lower than input buffer length."); - - ThrowIfDeviceInvalid(); - lock (_lock) - { - int length = HidApiNative.hid_read_timeout(_deviceHandle, buffer, _inputBufferLen, timeout); - return length; - } - } - - public Task RequestFeatureReportAsync(byte[] request) => Task.Run(() => RequestFeatureReport(request)); - public byte[] RequestFeatureReport(byte[] request) - { - if (request.Length > _inputBufferLen) - throw new ArgumentException("Request length is greater than input buffer length."); - - ThrowIfDeviceInvalid(); - - byte[] request_full = new byte[_inputBufferLen + 1]; - Array.Copy(request, 0, request_full, 1, request.Length); - byte[] response = new byte[_inputBufferLen + 1]; - - int err = HidApiNative.hid_send_feature_report(_deviceHandle, request_full, (uint)(_inputBufferLen + 1)); - if (err < 0) - throw new Exception($"Could not send report to hid device. Error: {err}"); - - err = HidApiNative.hid_get_feature_report(_deviceHandle, response, (uint)(_inputBufferLen + 1)); - if (err < 0) - throw new Exception($"Could not get report from hid device. Error: {err}"); - - return response; - } - - public Task WriteAsync(byte[] data) => Task.Run(() => Write(data)); - public void Write(byte[] data) - { - if (data.Length > _inputBufferLen) - throw new ArgumentException("Data length is greater than input buffer length."); - - ThrowIfDeviceInvalid(); - byte[] buffer = new byte[_inputBufferLen]; - Array.Copy(data, buffer, data.Length); - - int err = HidApiNative.hid_write(_deviceHandle, buffer, (uint)buffer.Length); - if (err < 0) - throw new Exception($"Failed to write to HID device. Error: {err}"); - } - - private void ReadLoop() - { - byte[] buffer = new byte[_inputBufferLen]; - while (_reading) - if (Read(buffer) > 0 && OnInputReceived != null) - _ = OnInputReceived(new HidDeviceInputReceivedEventArgs(this, buffer)); - } - - public void BeginRead() - { - _reading = true; - _readThread = new Thread(new ThreadStart(ReadLoop)); - _readThread.IsBackground = true; - _readThread.Start(); - } - - public void EndRead() - { - // kill read thread - if (_readThread != null) - { - _reading = false; - // Ensure the thread has finished execution - if (_readThread.IsAlive) - _readThread.Join(); - _readThread = null; - } - } - - public void Close() - { - HidApiNative.hid_close(_deviceHandle); - } - - public void Dispose() - { - Close(); - } - } -} +using hidapi.Native; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace hidapi +{ + public class HidDevice : IDisposable + { + private ushort _vid, _pid, _inputBufferLen; + private short _mi; + private IntPtr _deviceHandle; + private object _lock = new object(); + private bool _reading = false; + private Thread _readThread; + private long MillisecondsSinceEpoch => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + public bool IsDeviceValid => _deviceHandle != IntPtr.Zero; + public bool Reading => _reading; + public Func OnInputReceived; + + public HidDevice(ushort vendorId, ushort productId, ushort inputBufferLen = 64, short mi = -1) + { + _vid = vendorId; + _pid = productId; + _inputBufferLen = inputBufferLen; + _mi = mi; + } + + private void ThrowIfDeviceInvalid() + { + if (!IsDeviceValid) + throw new HidDeviceInvalidException(); + } + + private static short GetMI(string path) + { + string low = path.ToLower(); + int index = low.IndexOf("mi_"); + if (index == -1) + return -1; + string mi = low.Substring(index + 3, 2); + + if (short.TryParse(mi, out short number)) + return number; + + return -1; + } + + public Task OpenDeviceAsync() => Task.Run(() => OpenDevice()); + public bool OpenDevice() + { + lock (_lock) + { + IntPtr devEnum = HidApiNative.hid_enumerate(_vid, _pid); + IntPtr deviceInfo = devEnum; + + while (deviceInfo != IntPtr.Zero) + { + HidDeviceInfo hidDeviceInfo = new HidDeviceInfo(deviceInfo); + if (_mi != -1 && _mi != GetMI(hidDeviceInfo.Path)) + goto next; + + _deviceHandle = HidApiNative.hid_open_path(hidDeviceInfo.Path); + if (_deviceHandle != IntPtr.Zero) + break; + + next: + deviceInfo = hidDeviceInfo.NextDevicePtr; + } + + HidApiNative.hid_free_enumeration(deviceInfo); + return _deviceHandle != IntPtr.Zero; + } + } + + public Task ReadAsync(int timeout = 100) => Task.Run(() => Read(timeout)); + public byte[] Read(int timeout = 100) + { + ThrowIfDeviceInvalid(); + lock (_lock) + { + byte[] buffer = new byte[_inputBufferLen]; + int length = HidApiNative.hid_read_timeout(_deviceHandle, buffer, (uint)buffer.Length, timeout); + return buffer; + } + } + + public Task ReadAsync(byte[] data) => Task.Run(() => Read(data)); + public int Read(byte[] buffer, int timeout = 100) + { + if (buffer.Length < _inputBufferLen) + throw new ArgumentException("Buffer length is lower than input buffer length."); + + ThrowIfDeviceInvalid(); + lock (_lock) + { + int length = HidApiNative.hid_read_timeout(_deviceHandle, buffer, _inputBufferLen, timeout); + return length; + } + } + + public Task RequestFeatureReportAsync(byte[] request) => Task.Run(() => RequestFeatureReport(request)); + public byte[] RequestFeatureReport(byte[] request) + { + if (request.Length > _inputBufferLen) + throw new ArgumentException("Request length is greater than input buffer length."); + + ThrowIfDeviceInvalid(); + + byte[] request_full = new byte[_inputBufferLen + 1]; + Array.Copy(request, 0, request_full, 1, request.Length); + byte[] response = new byte[_inputBufferLen + 1]; + + int err = HidApiNative.hid_send_feature_report(_deviceHandle, request_full, (uint)(_inputBufferLen + 1)); + if (err < 0) + throw new Exception($"Could not send report to hid device. Error: {err}"); + + err = HidApiNative.hid_get_feature_report(_deviceHandle, response, (uint)(_inputBufferLen + 1)); + if (err < 0) + throw new Exception($"Could not get report from hid device. Error: {err}"); + + return response; + } + + public Task WriteAsync(byte[] data) => Task.Run(() => Write(data)); + public void Write(byte[] data) + { + if (data.Length > _inputBufferLen) + throw new ArgumentException("Data length is greater than input buffer length."); + + ThrowIfDeviceInvalid(); + byte[] buffer = new byte[_inputBufferLen]; + Array.Copy(data, buffer, data.Length); + + int err = HidApiNative.hid_write(_deviceHandle, buffer, (uint)buffer.Length); + if (err < 0) + throw new Exception($"Failed to write to HID device. Error: {err}"); + } + + private void ReadLoop() + { + byte[] buffer = new byte[_inputBufferLen]; + while (_reading) + if (Read(buffer) > 0 && OnInputReceived != null) + _ = OnInputReceived(new HidDeviceInputReceivedEventArgs(this, buffer)); + } + + public void BeginRead() + { + _reading = true; + _readThread = new Thread(new ThreadStart(ReadLoop)); + _readThread.IsBackground = true; + _readThread.Start(); + } + + public void EndRead() + { + // kill read thread + if (_readThread != null) + { + _reading = false; + // Ensure the thread has finished execution + if (_readThread.IsAlive) + _readThread.Join(); + _readThread = null; + } + } + + public void Close() + { + HidApiNative.hid_close(_deviceHandle); + } + + public void Dispose() + { + Close(); + } + } +} diff --git a/redist/RTSSSetup735.exe b/redist/RTSSSetup735.exe deleted file mode 100644 index bea40e54f..000000000 Binary files a/redist/RTSSSetup735.exe and /dev/null differ