mirror of
https://github.com/terrymacdonald/DisplayMagician.git
synced 2024-08-30 18:32:20 +00:00
Added some logic to check if either the from or to profile conatins an NVIDIA surround screen. If not, then there is no need to apply a GridTopology as it is all single devices! Saves up to 15 seconds on swap over.
437 lines
18 KiB
C#
437 lines
18 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.ComponentModel.DataAnnotations;
|
||
using McMaster.Extensions.CommandLineUtils;
|
||
using System.Diagnostics;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Net;
|
||
using System.Reflection;
|
||
using System.Runtime.CompilerServices;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using System.Windows.Forms;
|
||
using HeliosPlus.InterProcess;
|
||
using HeliosPlus.Resources;
|
||
using HeliosPlus.GameLibraries;
|
||
using HeliosPlus.Shared;
|
||
using HeliosPlus.UIForms;
|
||
using System.Net.NetworkInformation;
|
||
using System.Text.RegularExpressions;
|
||
using System.Drawing;
|
||
using System.Diagnostics.Contracts;
|
||
|
||
namespace HeliosPlus {
|
||
public enum SupportedProgramMode
|
||
{
|
||
RunShortcut,
|
||
EditProfile,
|
||
StartUpNormally
|
||
}
|
||
|
||
public enum SupportedGameLibrary
|
||
{
|
||
Unknown,
|
||
Steam,
|
||
Uplay
|
||
}
|
||
|
||
internal static class Program
|
||
{
|
||
|
||
internal static string AppDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "HeliosPlus");
|
||
public static string AppIconPath = Path.Combine(Program.AppDataPath, $"Icons");
|
||
public static string AppProfilePath = Path.Combine(Program.AppDataPath, $"Profiles");
|
||
public static string AppShortcutPath = Path.Combine(Program.AppDataPath, $"Shortcuts");
|
||
public static string AppHeliosPlusIconFilename = Path.Combine(AppIconPath, @"HeliosPlus.ico");
|
||
public static string AppOriginIconFilename = Path.Combine(AppIconPath, @"Origin.ico");
|
||
public static string AppSteamIconFilename = Path.Combine(AppIconPath, @"Steam.ico");
|
||
public static string AppUplayIconFilename = Path.Combine(AppIconPath, @"Steam.ico");
|
||
public static string AppEpicIconFilename = Path.Combine(AppIconPath, @"Epic.ico");
|
||
//internal static string ShortcutIconCachePath;
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// The main entry point for the application.
|
||
/// </summary>
|
||
[STAThread]
|
||
private static int Main(string[] args)
|
||
{
|
||
|
||
// Write the Application Name
|
||
Console.WriteLine($"{Application.ProductName} v{Application.ProductVersion}");
|
||
for (int i = 0; i <= Application.ProductName.Length + Application.ProductVersion .Length; i++)
|
||
{
|
||
Console.Write("=");
|
||
}
|
||
Console.WriteLine("=");
|
||
Console.WriteLine($"Copyright <20> Terry MacDonald 2020-{DateTime.Today.Year}");
|
||
Console.WriteLine(@"Based on Helios Display Management - Copyright <20> Soroush Falahati 2017-2020");
|
||
|
||
var app = new CommandLineApplication();
|
||
|
||
//app.Name = "HeliosDM+";
|
||
//app.Name = Assembly.GetEntryAssembly().GetName().Name;
|
||
app.Description = "This application helps configure your NVIDIA Videocard for multiple displays.";
|
||
app.ExtendedHelpText = "This application helps configure your NVIDIA Videocard for multiple displays. It has some nifty features such as the "
|
||
+ Environment.NewLine + " ability to temporarily change your screen settings while you are playing a game, and then change them back once finished.";
|
||
|
||
app.GetFullNameAndVersion();
|
||
app.MakeSuggestionsInErrorMessage = true;
|
||
app.HelpOption("-?|-h|--help", inherited:true);
|
||
|
||
app.VersionOption("-v|--version", () => {
|
||
return string.Format("Version {0}", Assembly.GetExecutingAssembly().GetName().Version);
|
||
});
|
||
|
||
// This is the RunShortcut command
|
||
app.Command(SupportedProgramMode.RunShortcut.ToString(), (switchProfileCmd) =>
|
||
{
|
||
var argumentShortcut = switchProfileCmd.Argument("\"SHORTCUT_UUID\"", "(required) The UUID of the shortcut to run from those stored in the shortcut library.").IsRequired();
|
||
argumentShortcut.Validators.Add(new ShortcutMustExistValidator());
|
||
|
||
//description and help text of the command.
|
||
switchProfileCmd.Description = "Use this command to run favourite game or application with a display profile of your choosing.";
|
||
|
||
switchProfileCmd.OnExecute(() =>
|
||
{
|
||
//
|
||
RunShortcut(argumentShortcut.Value);
|
||
return 0;
|
||
});
|
||
});
|
||
|
||
app.OnExecute(() =>
|
||
{
|
||
|
||
Console.WriteLine("Starting Normally...");
|
||
StartUpNormally();
|
||
return 0;
|
||
});
|
||
|
||
|
||
try
|
||
{
|
||
// This begins the actual execution of the application
|
||
app.Execute(args);
|
||
}
|
||
catch (CommandParsingException ex)
|
||
{
|
||
Console.WriteLine($"Program/Main commandParsingException: {ex.Message}: {ex.StackTrace} - {ex.InnerException}");
|
||
// You'll always want to catch this exception, otherwise it will generate a messy and confusing error for the end user.
|
||
// the message will usually be something like:
|
||
// "Unrecognized command or argument '<invalid-command>'"
|
||
Console.WriteLine(ex.Message);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"Program/Main exception: {ex.Message}: {ex.StackTrace} - {ex.InnerException}");
|
||
Console.WriteLine("Unable to execute application: {0}", ex.Message);
|
||
}
|
||
return 0;
|
||
//return app.Execute(args);
|
||
}
|
||
|
||
private static void StartUpNormally()
|
||
{
|
||
|
||
|
||
Application.EnableVisualStyles();
|
||
Application.SetCompatibleTextRenderingDefault(false);
|
||
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
|
||
|
||
try
|
||
{
|
||
// Start the IPC Service to
|
||
if (!IPCService.StartService())
|
||
{
|
||
throw new Exception(Language.Can_not_open_a_named_pipe_for_Inter_process_communication);
|
||
}
|
||
|
||
|
||
// Create the Shortcut Icon Cache if it doesn't exist so that it's avilable for all the program
|
||
if (!Directory.Exists(AppIconPath))
|
||
{
|
||
try
|
||
{
|
||
Directory.CreateDirectory(AppIconPath);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"Program/StartUpNormally exception: {ex.Message}: {ex.StackTrace} - {ex.InnerException}");
|
||
// TODO
|
||
}
|
||
}
|
||
|
||
|
||
// Create the Shortcut Icon Cache if it doesn't exist so that it's avilable for all the program
|
||
if (!Directory.Exists(AppIconPath))
|
||
{
|
||
try
|
||
{
|
||
Directory.CreateDirectory(AppIconPath);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"Program/StartUpNormally exception: {ex.Message}: {ex.StackTrace} - {ex.InnerException}");
|
||
// TODO
|
||
}
|
||
}
|
||
|
||
try
|
||
{
|
||
// Save a copy of the HeliosPlus Icon, and all the game library ones in preparation for future use
|
||
if (!File.Exists(AppHeliosPlusIconFilename))
|
||
{
|
||
Icon heliosIcon = (Icon)Properties.Resources.HeliosPlus;
|
||
using (FileStream fs = new FileStream(AppHeliosPlusIconFilename, FileMode.Create))
|
||
heliosIcon.Save(fs);
|
||
}
|
||
|
||
// Save a copy of the Steam Icon, and all the game library ones in preparation for future use
|
||
if (!File.Exists(AppSteamIconFilename))
|
||
{
|
||
Icon heliosIcon = (Icon)Properties.Resources.Steam;
|
||
using (FileStream fs = new FileStream(AppSteamIconFilename, FileMode.Create))
|
||
heliosIcon.Save(fs);
|
||
}
|
||
|
||
// Save a copy of the Uplay Icon, and all the game library ones in preparation for future use
|
||
if (!File.Exists(AppUplayIconFilename))
|
||
{
|
||
Icon heliosIcon = (Icon)Properties.Resources.Uplay;
|
||
using (FileStream fs = new FileStream(AppUplayIconFilename, FileMode.Create))
|
||
heliosIcon.Save(fs);
|
||
}
|
||
|
||
// Save a copy of the Epic Icon, and all the game library ones in preparation for future use
|
||
if (!File.Exists(AppEpicIconFilename))
|
||
{
|
||
Icon heliosIcon = (Icon)Properties.Resources.Epic;
|
||
using (FileStream fs = new FileStream(AppEpicIconFilename, FileMode.Create))
|
||
heliosIcon.Save(fs);
|
||
}
|
||
|
||
// Save a copy of the Origin Icon, and all the game library ones in preparation for future use
|
||
if (!File.Exists(AppOriginIconFilename))
|
||
{
|
||
Icon heliosIcon = (Icon)Properties.Resources.Origin;
|
||
using (FileStream fs = new FileStream(AppOriginIconFilename, FileMode.Create))
|
||
heliosIcon.Save(fs);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"Program/StartUpNormally exception 2: {ex.Message}: {ex.StackTrace} - {ex.InnerException}");
|
||
// TODO
|
||
}
|
||
|
||
IPCService.GetInstance().Status = InstanceStatus.User;
|
||
Application.Run(new UIForms.MainForm());
|
||
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"Program/StartUpNormally exception 3: {ex.Message}: {ex.StackTrace} - {ex.InnerException}");
|
||
MessageBox.Show(
|
||
ex.Message,
|
||
Language.Fatal_Error,
|
||
MessageBoxButtons.OK,
|
||
MessageBoxIcon.Error);
|
||
}
|
||
|
||
}
|
||
|
||
|
||
// ReSharper disable once CyclomaticComplexity
|
||
private static void RunShortcut(string shortcutUUID)
|
||
{
|
||
ShortcutItem shortcutToRun = null;
|
||
|
||
// Check there is only one version of this application so we won't
|
||
// mess with another monitoring session
|
||
if (
|
||
IPCClient.QueryAll()
|
||
.Any(
|
||
client =>
|
||
client.Status == InstanceStatus.Busy ||
|
||
client.Status == InstanceStatus.OnHold))
|
||
{
|
||
throw new Exception(
|
||
Language
|
||
.Another_instance_of_this_program_is_in_working_state_Please_close_other_instances_before_trying_to_switch_profile);
|
||
}
|
||
|
||
// Match the ShortcutName to the actual shortcut listed in the shortcut library
|
||
// And error if we can't find it.
|
||
if (ShortcutRepository.ContainsShortcut(shortcutUUID))
|
||
{
|
||
// make sure we trim the "" if there are any
|
||
shortcutUUID = shortcutUUID.Trim('"');
|
||
shortcutToRun = ShortcutRepository.GetShortcut(shortcutUUID);
|
||
}
|
||
else
|
||
{
|
||
throw new Exception(Language.Cannot_find_shortcut_in_library);
|
||
}
|
||
|
||
if (shortcutToRun is ShortcutItem)
|
||
{
|
||
ShortcutRepository.RunShortcut(shortcutToRun);
|
||
}
|
||
|
||
IPCService.GetInstance().Status = InstanceStatus.Busy;
|
||
|
||
}
|
||
|
||
public static bool IsValidFilename(string testName)
|
||
{
|
||
string strTheseAreInvalidFileNameChars = new string(Path.GetInvalidFileNameChars());
|
||
Regex regInvalidFileName = new Regex("[" + Regex.Escape(strTheseAreInvalidFileNameChars) + "]");
|
||
|
||
if (regInvalidFileName.IsMatch(testName)) { return false; };
|
||
|
||
return true;
|
||
}
|
||
|
||
// ApplyProfile lives here so that the UI works.
|
||
public static bool ApplyProfile(ProfileItem profile)
|
||
{
|
||
// If we're already on the wanted profile then no need to change!
|
||
if (ProfileRepository.IsActiveProfile(profile))
|
||
return true;
|
||
|
||
// We need to check if the profile is valid
|
||
if (!profile.IsPossible)
|
||
return false;
|
||
|
||
try
|
||
{
|
||
// Now lets prepare changing the display topology task
|
||
Task applyTopologyTask = new Task(() =>
|
||
{
|
||
Console.WriteLine("Program/ApplyProfile : Applying Profile Topology " + profile.Name);
|
||
if (!ProfileRepository.ApplyNVIDIAGridTopology(profile))
|
||
{
|
||
// Somehow return that this profile topology didn't apply
|
||
}
|
||
});
|
||
|
||
Task applyPathInfoTask = new Task(() => {
|
||
Console.WriteLine("Program/ApplyProfile : Applying Profile Path " + profile.Name);
|
||
if (!ProfileRepository.ApplyWindowsDisplayPathInfo(profile))
|
||
{
|
||
// Somehow return that this profile path info didn't apply
|
||
}
|
||
|
||
});
|
||
|
||
// Set up the UI forms to show
|
||
ApplyingProfileForm timeoutForm = new ApplyingProfileForm(null, 3, $"Changing to '{profile.Name}' Profile", "Press ESC to cancel", Color.Orange, true);
|
||
ApplyingProfileForm topologyForm = new ApplyingProfileForm(applyTopologyTask, 15, $"Changing to '{profile.Name}' Profile", "Applying NVIDIA Grid Topology", Color.Aquamarine);
|
||
ApplyingProfileForm pathInfoForm = new ApplyingProfileForm(applyPathInfoTask, 15, $"Changing to '{profile.Name}' Profile", "Applying Windows Display Device settings", Color.LawnGreen);
|
||
|
||
if (timeoutForm.ShowDialog() == DialogResult.Cancel)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// We only want to do the topology change if the profile we're on now
|
||
// or the profile we're going to are NVIDIA surround profiles
|
||
int toProfileSurroundTopologyCount =
|
||
profile.Paths.SelectMany(paths => paths.TargetDisplays)
|
||
.Select(target => target.SurroundTopology)
|
||
.Where(topology => topology != null)
|
||
.Select(topology => topology.ToGridTopology())
|
||
.Count();
|
||
int fromProfileSurroundTopologyCount =
|
||
ProfileRepository.CurrentProfile.Paths.SelectMany(paths => paths.TargetDisplays)
|
||
.Select(target => target.SurroundTopology)
|
||
.Where(topology => topology != null)
|
||
.Select(topology => topology.ToGridTopology())
|
||
.Count();
|
||
|
||
if (toProfileSurroundTopologyCount > 0 || fromProfileSurroundTopologyCount > 0)
|
||
{
|
||
topologyForm.ShowDialog();
|
||
|
||
try
|
||
{
|
||
applyTopologyTask.Wait();
|
||
}
|
||
catch (AggregateException ae)
|
||
{
|
||
foreach (var e in ae.InnerExceptions)
|
||
{
|
||
// Handle the custom exception.
|
||
if (e is ApplyTopologyException)
|
||
{
|
||
Console.WriteLine(e.Message);
|
||
}
|
||
// Rethrow any other exception.
|
||
else
|
||
{
|
||
throw;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (applyTopologyTask.IsFaulted)
|
||
Console.WriteLine("Program/ApplyProfile : Applying Profile Topology stage failed to complete");
|
||
}
|
||
|
||
// We always want to do the WindowsDisplayAPI PathInfo part
|
||
pathInfoForm.ShowDialog();
|
||
applyPathInfoTask.Wait();
|
||
try
|
||
{
|
||
applyPathInfoTask.Wait();
|
||
}
|
||
catch (AggregateException ae)
|
||
{
|
||
foreach (var e in ae.InnerExceptions)
|
||
{
|
||
// Handle the custom exception.
|
||
if (e is ApplyPathInfoException)
|
||
{
|
||
Console.WriteLine(e.Message);
|
||
}
|
||
// Rethrow any other exception.
|
||
else
|
||
{
|
||
throw;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (applyPathInfoTask.IsFaulted)
|
||
Console.WriteLine("Program/ApplyProfile : Applying Profile PathInfo stage failed to complete");
|
||
|
||
if (applyTopologyTask.IsCompleted && applyPathInfoTask.IsCompleted)
|
||
return true;
|
||
else
|
||
return false;
|
||
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"ProfileRepository/ApplyTopology exception: {ex.Message}: {ex.StackTrace} - {ex.InnerException}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
public class ApplyTopologyException : Exception
|
||
{
|
||
public ApplyTopologyException(String message) : base(message)
|
||
{ }
|
||
}
|
||
|
||
public class ApplyPathInfoException : Exception
|
||
{
|
||
public ApplyPathInfoException(String message) : base(message)
|
||
{ }
|
||
}
|
||
} |