This commit is contained in:
seraphima
2024-05-21 16:55:15 +02:00
parent 483f7aa464
commit c157e64f28
73 changed files with 7107 additions and 0 deletions

View File

@@ -0,0 +1,74 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34622.214
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ProjectsSnapshotTool", "ProjectsSnapshotTool\ProjectsSnapshotTool.vcxproj", "{3D63307B-9D27-44FD-B033-B26F39245B85}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "projects-common", "projects-common", "{BA45247D-3046-408D-BE01-128587A7799F}"
ProjectSection(SolutionItems) = preProject
projects-common\Data.h = projects-common\Data.h
projects-common\GuidUtils.h = projects-common\GuidUtils.h
projects-common\json.h = projects-common\json.h
projects-common\MonitorEnumerator.h = projects-common\MonitorEnumerator.h
projects-common\WindowEnumerator.h = projects-common\WindowEnumerator.h
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ProjectsLauncher", "ProjectsLauncher\ProjectsLauncher.vcxproj", "{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProjectsEditor", "ProjectsEditor\ProjectsEditor.csproj", "{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|Any CPU.ActiveCfg = Debug|x64
{3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|Any CPU.Build.0 = Debug|x64
{3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|x64.ActiveCfg = Debug|x64
{3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|x64.Build.0 = Debug|x64
{3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|x86.ActiveCfg = Debug|Win32
{3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|x86.Build.0 = Debug|Win32
{3D63307B-9D27-44FD-B033-B26F39245B85}.Release|Any CPU.ActiveCfg = Release|x64
{3D63307B-9D27-44FD-B033-B26F39245B85}.Release|Any CPU.Build.0 = Release|x64
{3D63307B-9D27-44FD-B033-B26F39245B85}.Release|x64.ActiveCfg = Release|x64
{3D63307B-9D27-44FD-B033-B26F39245B85}.Release|x64.Build.0 = Release|x64
{3D63307B-9D27-44FD-B033-B26F39245B85}.Release|x86.ActiveCfg = Release|Win32
{3D63307B-9D27-44FD-B033-B26F39245B85}.Release|x86.Build.0 = Release|Win32
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|Any CPU.ActiveCfg = Debug|x64
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|Any CPU.Build.0 = Debug|x64
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|x64.ActiveCfg = Debug|x64
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|x64.Build.0 = Debug|x64
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|x86.ActiveCfg = Debug|Win32
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|x86.Build.0 = Debug|Win32
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|Any CPU.ActiveCfg = Release|x64
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|Any CPU.Build.0 = Release|x64
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x64.ActiveCfg = Release|x64
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x64.Build.0 = Release|x64
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x86.ActiveCfg = Release|Win32
{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x86.Build.0 = Release|Win32
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|x64.ActiveCfg = Debug|x64
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|x64.Build.0 = Debug|x64
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|x86.ActiveCfg = Debug|Any CPU
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|x86.Build.0 = Debug|Any CPU
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|Any CPU.Build.0 = Release|Any CPU
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|x64.ActiveCfg = Release|x64
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|x64.Build.0 = Release|x64
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|x86.ActiveCfg = Release|Any CPU
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BE6AD818-2650-419C-8FDE-535C22ED09B3}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>
</startup>
<runtime>
<AppContextSwitchOverrides value = "Switch.System.Windows.DoNotScaleForDpiChanges=false"/>
</runtime>
</configuration>

View File

@@ -0,0 +1,20 @@
<Application
x:Class="ProjectsEditor.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ProjectsEditor"
xmlns:ui="http://schemas.modernwpf.com/2019"
Exit="OnExit"
Startup="OnStartup">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ThemeResources />
<ui:XamlControlsResources />
<ResourceDictionary Source="pack://application:,,,/Styles/ButtonStyles.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style x:Key="HeadingTextBlock" TargetType="TextBlock" />
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,98 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Windows;
using ProjectsEditor.Common;
using ProjectsEditor.Utils;
using ProjectsEditor.ViewModels;
namespace ProjectsEditor
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application, IDisposable
{
public static ProjectsEditorIO ProjectsEditorIO { get; private set; }
private MainWindow _mainWindow;
private MainViewModel _mainViewModel;
private ThemeManager _themeManager;
private bool _isDisposed;
public App()
{
ProjectsEditorIO = new ProjectsEditorIO();
}
private void OnStartup(object sender, StartupEventArgs e)
{
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
_themeManager = new ThemeManager(this);
if (_mainViewModel == null)
{
_mainViewModel = new MainViewModel(ProjectsEditorIO);
}
var parseResult = ProjectsEditorIO.ParseProjects(_mainViewModel);
string[] args = Environment.GetCommandLineArgs();
if (args != null && args.Length > 1)
{
_mainViewModel.LaunchProject(args[1]);
return;
}
// normal start of editor
if (_mainWindow == null)
{
_mainWindow = new MainWindow(_mainViewModel);
}
// reset main window owner to keep it on the top
_mainWindow.ShowActivated = true;
_mainWindow.Topmost = true;
_mainWindow.Show();
// we can reset topmost flag after it's opened
_mainWindow.Topmost = false;
}
private void OnExit(object sender, ExitEventArgs e)
{
Dispose();
}
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs args)
{
// TODO: log the error and show an error message
}
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
_themeManager?.Dispose();
}
_isDisposed = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using ControlzEx.Theming;
namespace ProjectsEditor.Common
{
public class CustomLibraryThemeProvider : LibraryThemeProvider
{
public static readonly CustomLibraryThemeProvider DefaultInstance = new CustomLibraryThemeProvider();
public CustomLibraryThemeProvider()
: base(true)
{
}
/// <inheritdoc />
public override void FillColorSchemeValues(Dictionary<string, string> values, RuntimeThemeColorValues colorValues)
{
}
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace ProjectsEditor.Common
{
public enum Theme
{
System,
Light,
Dark,
HighContrastOne,
HighContrastTwo,
HighContrastBlack,
HighContrastWhite,
}
public enum AppTheme
{
Dark = 0,
Light = 1,
}
}

View File

@@ -0,0 +1,205 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// using ManagedCommon;
using System;
using System.Linq;
using System.Windows;
using Microsoft.Win32;
namespace ProjectsEditor.Common
{
public class ThemeManager : IDisposable
{
private readonly Application _app;
private const string LightTheme = "Light.Accent1";
private const string DarkTheme = "Dark.Accent1";
private const string HighContrastOneTheme = "HighContrast.Accent2";
private const string HighContrastTwoTheme = "HighContrast.Accent3";
private const string HighContrastBlackTheme = "HighContrast.Accent4";
private const string HighContrastWhiteTheme = "HighContrast.Accent5";
private Theme _currentTheme;
private Theme _settingsTheme;
private bool _disposed;
public event ThemeChangedHandler ThemeChanged;
public ThemeManager(Application app)
{
_app = app;
Uri highContrastOneThemeUri = new Uri("pack://application:,,,/Themes/HighContrast1.xaml");
Uri highContrastTwoThemeUri = new Uri("pack://application:,,,/Themes/HighContrast2.xaml");
Uri highContrastBlackThemeUri = new Uri("pack://application:,,,/Themes/HighContrastWhite.xaml");
Uri highContrastWhiteThemeUri = new Uri("pack://application:,,,/Themes/HighContrastBlack.xaml");
Uri lightThemeUri = new Uri("pack://application:,,,/Themes/Light.xaml");
Uri darkThemeUri = new Uri("pack://application:,,,/Themes/Dark.xaml");
ControlzEx.Theming.ThemeManager.Current.AddLibraryTheme(
new ControlzEx.Theming.LibraryTheme(
highContrastOneThemeUri,
CustomLibraryThemeProvider.DefaultInstance));
ControlzEx.Theming.ThemeManager.Current.AddLibraryTheme(
new ControlzEx.Theming.LibraryTheme(
highContrastTwoThemeUri,
CustomLibraryThemeProvider.DefaultInstance));
ControlzEx.Theming.ThemeManager.Current.AddLibraryTheme(
new ControlzEx.Theming.LibraryTheme(
highContrastBlackThemeUri,
CustomLibraryThemeProvider.DefaultInstance));
ControlzEx.Theming.ThemeManager.Current.AddLibraryTheme(
new ControlzEx.Theming.LibraryTheme(
highContrastWhiteThemeUri,
CustomLibraryThemeProvider.DefaultInstance));
ControlzEx.Theming.ThemeManager.Current.AddLibraryTheme(
new ControlzEx.Theming.LibraryTheme(
lightThemeUri,
CustomLibraryThemeProvider.DefaultInstance));
ControlzEx.Theming.ThemeManager.Current.AddLibraryTheme(
new ControlzEx.Theming.LibraryTheme(
darkThemeUri,
CustomLibraryThemeProvider.DefaultInstance));
ResetTheme();
ControlzEx.Theming.ThemeManager.Current.ThemeSyncMode = ControlzEx.Theming.ThemeSyncMode.SyncWithAppMode;
ControlzEx.Theming.ThemeManager.Current.ThemeChanged += Current_ThemeChanged;
SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged;
}
private void SystemParameters_StaticPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SystemParameters.HighContrast))
{
ResetTheme();
}
}
public Theme GetCurrentTheme()
{
return _currentTheme;
}
private static Theme GetHighContrastBaseType()
{
string registryKey = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes";
string theme = (string)Registry.GetValue(registryKey, "CurrentTheme", string.Empty);
theme = theme.Split('\\').Last().Split('.').First().ToString();
switch (theme)
{
case "hc1":
return Theme.HighContrastOne;
case "hc2":
return Theme.HighContrastTwo;
case "hcwhite":
return Theme.HighContrastWhite;
case "hcblack":
return Theme.HighContrastBlack;
default:
return Theme.HighContrastOne;
}
}
private void ResetTheme()
{
ChangeTheme(_settingsTheme == Theme.System ? Theme.System : _currentTheme);
}
public static string GetWindowsBaseColor()
{
return ControlzEx.Theming.WindowsThemeHelper.GetWindowsBaseColor();
}
public void ChangeTheme(Theme theme, bool fromSettings = false)
{
if (fromSettings)
{
_settingsTheme = theme;
}
Theme oldTheme = _currentTheme;
if (theme == Theme.System)
{
_currentTheme = Theme.System;
if (ControlzEx.Theming.WindowsThemeHelper.IsHighContrastEnabled())
{
Theme highContrastBaseType = GetHighContrastBaseType();
ChangeTheme(highContrastBaseType, false);
}
else
{
string baseColor = ControlzEx.Theming.WindowsThemeHelper.GetWindowsBaseColor();
ChangeTheme((Theme)Enum.Parse(typeof(Theme), baseColor));
}
}
else if (theme == Theme.HighContrastOne)
{
_currentTheme = Theme.HighContrastOne;
ControlzEx.Theming.ThemeManager.Current.ChangeTheme(_app, HighContrastOneTheme);
}
else if (theme == Theme.HighContrastTwo)
{
_currentTheme = Theme.HighContrastTwo;
ControlzEx.Theming.ThemeManager.Current.ChangeTheme(_app, HighContrastTwoTheme);
}
else if (theme == Theme.HighContrastWhite)
{
_currentTheme = Theme.HighContrastWhite;
ControlzEx.Theming.ThemeManager.Current.ChangeTheme(_app, HighContrastWhiteTheme);
}
else if (theme == Theme.HighContrastBlack)
{
_currentTheme = Theme.HighContrastBlack;
ControlzEx.Theming.ThemeManager.Current.ChangeTheme(_app, HighContrastBlackTheme);
}
else if (theme == Theme.Light)
{
_currentTheme = Theme.Light;
ControlzEx.Theming.ThemeManager.Current.ChangeTheme(_app, LightTheme);
}
else if (theme == Theme.Dark)
{
_currentTheme = Theme.Dark;
ControlzEx.Theming.ThemeManager.Current.ChangeTheme(_app, DarkTheme);
}
ThemeChanged?.Invoke(oldTheme, _currentTheme);
}
private void Current_ThemeChanged(object sender, ControlzEx.Theming.ThemeChangedEventArgs e)
{
ControlzEx.Theming.ThemeManager.Current.ThemeChanged -= Current_ThemeChanged;
try
{
ResetTheme();
}
finally
{
ControlzEx.Theming.ThemeManager.Current.ThemeChanged += Current_ThemeChanged;
}
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
ControlzEx.Theming.ThemeManager.Current.ThemeChanged -= Current_ThemeChanged;
_disposed = true;
}
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
public delegate void ThemeChangedHandler(Theme oldTheme, Theme newTheme);
}

View File

@@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Projects.Data;
using static ProjectsEditor.Data.ProjectsData;
namespace ProjectsEditor.Data
{
public class ProjectsData : ProjectsEditorData<ProjectsListWrapper>
{
public string File
{
get
{
return GetDataFolder() + "\\projects.json";
}
}
public struct ApplicationWrapper
{
public struct WindowPositionWrapper
{
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
}
public long Hwnd { get; set; }
public string Application { get; set; }
public string Title { get; set; }
public string CommandLineArguments { get; set; }
public bool Minimized { get; set; }
public bool Maximized { get; set; }
public WindowPositionWrapper Position { get; set; }
public int Monitor { get; set; }
}
public struct MonitorConfigurationWrapper
{
public struct MonitorRectWrapper
{
public int Top { get; set; }
public int Left { get; set; }
public int Width { get; set; }
public int Height { get; set; }
}
public string Id { get; set; }
public string InstanceId { get; set; }
public int MonitorNumber { get; set; }
public int Dpi { get; set; }
public MonitorRectWrapper MonitorRectDpiAware { get; set; }
public MonitorRectWrapper MonitorRectDpiUnaware { get; set; }
}
public struct ProjectWrapper
{
public string Id { get; set; }
public string Name { get; set; }
public long CreationTime { get; set; }
public long LastLaunchedTime { get; set; }
public bool IsShortcutNeeded { get; set; }
public List<MonitorConfigurationWrapper> MonitorConfiguration { get; set; }
public List<ApplicationWrapper> Applications { get; set; }
}
public struct ProjectsListWrapper
{
public List<ProjectWrapper> Projects { get; set; }
}
public enum OrderBy
{
LastViewed = 0,
Created = 1,
Name = 2,
Unknown = 3,
}
}
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Text.Json;
using ProjectsEditor.Utils;
namespace Projects.Data
{
public class ProjectsEditorData<T>
{
// Note: the same path should be used in SnapshotTool and Launcher
public string GetDataFolder()
{
// return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
}
public string GetTempDataFolder()
{
return Path.GetTempPath();
}
protected JsonSerializerOptions JsonOptions
{
get
{
return new JsonSerializerOptions
{
PropertyNamingPolicy = new DashCaseNamingPolicy(),
WriteIndented = true,
};
}
}
public T Read(string file)
{
IOUtils ioUtils = new IOUtils();
string data = ioUtils.ReadFile(file);
return JsonSerializer.Deserialize<T>(data, JsonOptions);
}
public string Serialize(T data)
{
return JsonSerializer.Serialize(data, JsonOptions);
}
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Controls;
namespace ProjectsEditor
{
public class HeadingTextBlock : TextBlock
{
protected override AutomationPeer OnCreateAutomationPeer()
{
return new HeadingTextBlockAutomationPeer(this);
}
internal sealed class HeadingTextBlockAutomationPeer : TextBlockAutomationPeer
{
public HeadingTextBlockAutomationPeer(HeadingTextBlock owner)
: base(owner)
{
}
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Header;
}
}
}
}

View File

@@ -0,0 +1,272 @@
<Page
x:Class="ProjectsEditor.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:props="clr-namespace:ProjectsEditor.Properties"
xmlns:local="clr-namespace:ProjectsEditor"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="MainPage">
<Page.Resources>
<Thickness x:Key="ContentDialogPadding">24,16,0,24</Thickness>
<Thickness x:Key="ContentDialogCommandSpaceMargin">0,24,24,0</Thickness>
<Style x:Key="CancelButtonStyle" TargetType="Button">
<Setter Property="Background" Value="#9d0808" />
<Setter Property="BorderBrush" Value="#FF262E34"/>
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}" BorderBrush="#FF262E34" BorderThickness="1">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#ec4d37"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#9d0808"/>
</Trigger>
</Style.Triggers>
</Style>
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<local:HeadingTextBlock
x:Name="ProjectsHeaderBlock"
AutomationProperties.HeadingLevel="Level1"
FontSize="24"
FontWeight="SemiBold"
Text="{x:Static props:Resources.Projects}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Grid.Row="0"
Margin="40,20,40,20"/>
<Button
x:Name="NewProjectButton"
Height="36"
Padding="0"
Grid.Row="0"
Margin="0,20,40,20"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
AutomationProperties.Name="{x:Static props:Resources.CreateProject}"
Click="NewProjectButton_Click"
Style="{StaticResource AccentButtonStyle}"
TabIndex="3">
<StackPanel Margin="12, 8, 12, 8" Orientation="Horizontal">
<TextBlock
AutomationProperties.Name="{x:Static props:Resources.CreateProject}"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource AccentButtonForeground}"
Text="&#xE710;" />
<TextBlock
Margin="12,-3,0,0"
Foreground="{DynamicResource AccentButtonForeground}"
Text="{x:Static props:Resources.CreateProject}" />
</StackPanel>
<Button.Effect>
<DropShadowEffect
BlurRadius="6"
Opacity="0.32"
ShadowDepth="1" />
</Button.Effect>
</Button>
<Border
HorizontalAlignment="Left"
Grid.Row="1"
Margin="40,0,0,0"
BorderThickness="2"
VerticalAlignment="Center"
CornerRadius="5">
<StackPanel
Orientation="Horizontal">
<Grid>
<TextBox
x:Name="SearchTextBox"
Width="320"
Text="{Binding SearchTerm, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Background="{DynamicResource SecondaryBackgroundBrush}"
BorderBrush="{DynamicResource PrimaryBorderBrush}"
/>
<TextBlock
IsHitTestVisible="False"
Text="{x:Static props:Resources.Search}"
VerticalAlignment="Center"
Foreground="{DynamicResource SecondaryForegroundBrush}"
Margin="10,0,0,0">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=SearchTextBox}" Value="">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
<TextBlock
HorizontalAlignment="Left"
VerticalAlignment="Center"
Margin="-24,0,10,0"
AutomationProperties.Name="{x:Static props:Resources.Search}"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource SecondaryForegroundBrush}"
Text="&#xE71E;" />
</StackPanel>
</Border>
<StackPanel
Grid.Row="1"
HorizontalAlignment="Right"
Orientation="Horizontal"
Margin="0,0,40,0">
<TextBlock
VerticalAlignment="Center"
Margin="10,0,10,0"
Text="{x:Static props:Resources.SortBy}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
/>
<ComboBox
Width="140"
Background="{DynamicResource SecondaryBackgroundBrush}"
BorderBrush="{DynamicResource PrimaryBorderBrush}"
SelectedIndex="{Binding OrderByIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ComboBoxItem Content="{x:Static props:Resources.LastLaunched}" />
<ComboBoxItem Content="{x:Static props:Resources.Created}" />
<ComboBoxItem Content="{x:Static props:Resources.Name}" />
</ComboBox>
</StackPanel>
<ScrollViewer
VerticalContentAlignment="Stretch"
VerticalScrollBarVisibility="Auto"
Grid.Row="2"
Margin="40,15,40,40">
<ItemsControl ItemsSource="{Binding ProjectsView, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
IsItemsHost="True"
Orientation="Vertical"
HorizontalAlignment="Stretch"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="models:Project">
<Button
x:Name="EditButton"
AutomationProperties.Name="{x:Static props:Resources.Edit}"
Margin="0,12,0,0"
Click="EditButtonClicked"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Background="{DynamicResource SecondaryBackgroundBrush}"
Padding="1">
<Border Background="{DynamicResource SecondaryBackgroundBrush}"
HorizontalAlignment="Stretch"
CornerRadius="5">
<DockPanel HorizontalAlignment="Stretch">
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch" Margin="12,14,10,10">
<TextBlock
Text="{Binding Name, Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"
FontSize="16"
Margin="0,0,0,8"
FontWeight="SemiBold"
HorizontalAlignment="Left"
VerticalAlignment="Center"/>
<StackPanel
Orientation="Horizontal"
VerticalAlignment="Center"
Margin="0,0,0,8" >
<Image
Source="{Binding PreviewImage, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Height="20" />
<TextBlock
Text="{Binding AppsCountString}"
Margin="6,0,4,0"
VerticalAlignment="Center"/>
</StackPanel>
<StackPanel
Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="&#xE81C;"
Margin="0,3,10,0"/>
<TextBlock Text="{Binding LastLaunched, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Vertical" DockPanel.Dock="Left" Margin="12,12,12,12">
<StackPanel
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button
x:Name="MoreButton"
HorizontalAlignment="Right"
Style="{StaticResource IconOnlyButtonStyle}"
Click="MoreButton_Click">
<TextBlock
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="&#xE712;"/>
</Button>
<Popup
IsOpen="{Binding IsPopupVisible, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
StaysOpen="False"
PlacementTarget="{Binding ElementName=MoreButton}"
Placement="Left">
<Button
AutomationProperties.Name="{x:Static props:Resources.Delete}"
Click="DeleteButtonClicked">
<StackPanel Orientation="Horizontal">
<TextBlock
AutomationProperties.Name="{x:Static props:Resources.Delete}"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource AccentButtonForeground}"
Text="&#xE74D;" />
<TextBlock
Margin="10,0,0,0"
AutomationProperties.Name="{x:Static props:Resources.Delete}"
Foreground="{DynamicResource AccentButtonForeground}"
Text="{x:Static props:Resources.Delete}" />
</StackPanel>
</Button>
</Popup>
</StackPanel>
<Button
Padding="20,4,20,4"
Margin="0,6,0,0"
AutomationProperties.Name="{x:Static props:Resources.Launch}"
Content="{x:Static props:Resources.Launch}"
HorizontalAlignment="Right"
Background="{DynamicResource TertiaryBackgroundBrush}"
BorderBrush="{DynamicResource SecondaryBorderBrush}"
BorderThickness="1"
Click="LaunchButton_Click"/>
</StackPanel>
</DockPanel>
</Border>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Windows;
using System.Windows.Controls;
using ProjectsEditor.Models;
using ProjectsEditor.ViewModels;
namespace ProjectsEditor
{
/// <summary>
/// Interaction logic for MainPage.xaml
/// </summary>
public partial class MainPage : Page
{
private MainViewModel _mainViewModel;
public MainPage(MainViewModel mainViewModel)
{
InitializeComponent();
_mainViewModel = mainViewModel;
this.DataContext = _mainViewModel;
}
private /*async*/ void NewProjectButton_Click(object sender, RoutedEventArgs e)
{
_mainViewModel.AddNewProject();
}
private void EditButtonClicked(object sender, RoutedEventArgs e)
{
Button button = sender as Button;
Project selectedProject = button.DataContext as Project;
_mainViewModel.EditProject(selectedProject);
}
private void DeleteButtonClicked(object sender, RoutedEventArgs e)
{
e.Handled = true;
Button button = sender as Button;
Project selectedProject = button.DataContext as Project;
_mainViewModel.DeleteProject(selectedProject);
}
private void MoreButton_Click(object sender, RoutedEventArgs e)
{
e.Handled = true;
Button button = sender as Button;
Project project = button.DataContext as Project;
project.IsPopupVisible = true;
}
private void LaunchButton_Click(object sender, RoutedEventArgs e)
{
e.Handled = true;
Button button = sender as Button;
Project project = button.DataContext as Project;
_mainViewModel.LaunchProject(project);
}
}
}

View File

@@ -0,0 +1,33 @@
<Window
x:Class="ProjectsEditor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:props="clr-namespace:ProjectsEditor.Properties"
xmlns:ui="http://schemas.modernwpf.com/2019"
x:Name="ProjectsMainWindow"
Title="{x:Static props:Resources.MainTitle}"
ui:TitleBar.Background="{DynamicResource PrimaryBackgroundBrush}"
ui:TitleBar.InactiveBackground="{DynamicResource TertiaryBackgroundBrush}"
ui:TitleBar.IsIconVisible="True"
ui:WindowHelper.UseModernWindowStyle="True"
MinWidth="700"
MinHeight="480"
AutomationProperties.Name="Projects Editor"
Closing="OnClosing"
ContentRendered="OnContentRendered"
ResizeMode="CanResize"
WindowStartupLocation="CenterOwner"
mc:Ignorable="d"
Background="{DynamicResource PrimaryBackgroundBrush}">
<Border
CornerRadius="20"
BorderThickness="1">
<Grid Margin="0,10,0,0">
<Frame
x:Name="ContentFrame"
NavigationUIVisibility="Hidden"/>
</Grid>
</Border>
</Window>

View File

@@ -0,0 +1,100 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Windows;
using System.Windows.Interop;
using ProjectsEditor.Utils;
using ProjectsEditor.ViewModels;
namespace ProjectsEditor
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private bool haveTriedToGetFocusAlready;
public MainViewModel MainViewModel { get; set; }
private static MainPage _mainPage;
public MainWindow(MainViewModel mainViewModel)
{
MainViewModel = mainViewModel;
mainViewModel.SetMainWindow(this);
InitializeComponent();
_mainPage = new MainPage(mainViewModel);
ContentFrame.Navigate(_mainPage);
MaxWidth = SystemParameters.PrimaryScreenWidth;
MaxHeight = SystemParameters.PrimaryScreenHeight;
}
private void BringToFront()
{
// Get the window handle of the Projects Editor window
IntPtr handle = new WindowInteropHelper(this).Handle;
// Get the handle of the window currently in the foreground
IntPtr foregroundWindowHandle = NativeMethods.GetForegroundWindow();
// Get the thread IDs of the current thread and the thread of the foreground window
uint currentThreadId = NativeMethods.GetCurrentThreadId();
uint activeThreadId = NativeMethods.GetWindowThreadProcessId(foregroundWindowHandle, IntPtr.Zero);
// Check if the active thread is different from the current thread
if (activeThreadId != currentThreadId)
{
// Attach the input processing mechanism of the current thread to the active thread
NativeMethods.AttachThreadInput(activeThreadId, currentThreadId, true);
// Set the Projects Editor window as the foreground window
NativeMethods.SetForegroundWindow(handle);
// Detach the input processing mechanism of the current thread from the active thread
NativeMethods.AttachThreadInput(activeThreadId, currentThreadId, false);
}
else
{
// Set the Projects Editor window as the foreground window
NativeMethods.SetForegroundWindow(handle);
}
// Bring the Projects Editor window to the foreground and activate it
NativeMethods.SwitchToThisWindow(handle, true);
haveTriedToGetFocusAlready = true;
}
private void OnClosing(object sender, EventArgs e)
{
App.Current.Shutdown();
}
// This is required to fix a WPF rendering bug when using custom chrome
private void OnContentRendered(object sender, EventArgs e)
{
if (!haveTriedToGetFocusAlready)
{
BringToFront();
}
InvalidateVisual();
}
public void ShowPage(ProjectEditor editPage)
{
ContentFrame.Navigate(editPage);
}
public void SwitchToMainView()
{
ContentFrame.GoBack();
}
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace ProjectsEditor.Models
{
public sealed class AppListDataTemplateSelector : System.Windows.Controls.DataTemplateSelector
{
public System.Windows.DataTemplate HeaderTemplate { get; set; }
public System.Windows.DataTemplate AppTemplate { get; set; }
public AppListDataTemplateSelector()
{
HeaderTemplate = new System.Windows.DataTemplate();
AppTemplate = new System.Windows.DataTemplate();
}
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
if (item is string)
{
return HeaderTemplate;
}
else
{
return AppTemplate;
}
}
}
}

View File

@@ -0,0 +1,252 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Windows.Media.Imaging;
namespace ProjectsEditor.Models
{
public class Application : INotifyPropertyChanged, IDisposable
{
public event PropertyChangedEventHandler PropertyChanged;
public Project Parent { get; set; }
public struct WindowPosition
{
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
}
public IntPtr Hwnd { get; set; }
public string AppPath { get; set; }
public string AppTitle { get; set; }
public string CommandLineArguments { get; set; }
public bool Minimized { get; set; }
public bool Maximized { get; set; }
[JsonIgnore]
public bool IsSelected { get; set; }
[JsonIgnore]
public bool IsHighlighted { get; set; }
[JsonIgnore]
public int RepeatIndex { get; set; }
[JsonIgnore]
public string RepeatIndexString
{
get
{
return RepeatIndex == 0 ? string.Empty : RepeatIndex.ToString(CultureInfo.InvariantCulture);
}
}
[JsonIgnore]
private Icon _icon = null;
[JsonIgnore]
public Icon Icon
{
get
{
if (_icon == null)
{
try
{
_icon = Icon.ExtractAssociatedIcon(AppPath);
}
catch (Exception)
{
_icon = new Icon(@"images\DefaultIcon.ico");
}
}
return _icon;
}
}
private BitmapImage _iconBitmapImage;
public BitmapImage IconBitmapImage
{
get
{
if (_iconBitmapImage == null)
{
try
{
Bitmap previewBitmap = new Bitmap(32, 32);
using (Graphics graphics = Graphics.FromImage(previewBitmap))
{
graphics.SmoothingMode = SmoothingMode.AntiAlias;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
graphics.DrawIcon(Icon, new Rectangle(0, 0, 32, 32));
}
using (var memory = new MemoryStream())
{
previewBitmap.Save(memory, ImageFormat.Png);
memory.Position = 0;
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memory;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.Freeze();
_iconBitmapImage = bitmapImage;
}
}
catch (Exception)
{
// todo
}
}
return _iconBitmapImage;
}
}
[JsonIgnore]
public string AppName
{
get
{
if (File.Exists(AppPath))
{
return Path.GetFileNameWithoutExtension(AppPath);
}
return AppPath.Split('\\').LastOrDefault();
}
}
public WindowPosition Position { get; set; }
private WindowPosition? _scaledPosition;
public WindowPosition ScaledPosition
{
get
{
if (_scaledPosition == null)
{
double scaleFactor = MonitorSetup.Dpi / 96.0;
_scaledPosition = new WindowPosition()
{
X = (int)(scaleFactor * Position.X),
Y = (int)(scaleFactor * Position.Y),
Height = (int)(scaleFactor * Position.Height),
Width = (int)(scaleFactor * Position.Width),
};
}
return _scaledPosition.Value;
}
}
public int MonitorNumber { get; set; }
private MonitorSetup _monitorSetup;
public MonitorSetup MonitorSetup
{
get
{
if (_monitorSetup == null)
{
_monitorSetup = Parent.Monitors.Where(x => x.MonitorNumber == MonitorNumber).FirstOrDefault();
}
return _monitorSetup;
}
}
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChanged?.Invoke(this, e);
}
internal bool IsMyAppPath(string path)
{
if (!IsPackagedApp)
{
return path.Equals(AppPath, StringComparison.Ordinal);
}
else
{
return path.Contains(PackagedName + "_", StringComparison.InvariantCultureIgnoreCase) && path.Contains(PackagedPublisherID, StringComparison.InvariantCultureIgnoreCase);
}
}
private bool? _isPackagedApp;
public string PackagedId { get; set; }
public string PackagedName { get; set; }
public string PackagedPublisherID { get; set; }
public string Aumid { get; set; }
public bool IsPackagedApp
{
get
{
if (_isPackagedApp == null)
{
if (!AppPath.StartsWith("C:\\Program Files\\WindowsApps\\", StringComparison.InvariantCultureIgnoreCase))
{
_isPackagedApp = false;
}
else
{
string appPath = AppPath.Replace("C:\\Program Files\\WindowsApps\\", string.Empty);
Regex packagedAppPathRegex = new Regex(@"(?<APPID>[^_]*)_\d+.\d+.\d+.\d+_x64__(?<PublisherID>[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
Match match = packagedAppPathRegex.Match(appPath);
_isPackagedApp = match.Success;
if (match.Success)
{
PackagedName = match.Groups["APPID"].Value;
PackagedPublisherID = match.Groups["PublisherID"].Value;
PackagedId = $"{PackagedName}_{PackagedPublisherID}";
Aumid = $"{PackagedId}!App";
}
}
}
return _isPackagedApp.Value;
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Windows;
namespace ProjectsEditor.Models
{
public class Monitor
{
public string MonitorName { get; private set; }
public string MonitorInstanceId { get; private set; }
public int MonitorNumber { get; private set; }
public int Dpi { get; private set; }
public Rect MonitorDpiUnawareBounds { get; private set; }
public Rect MonitorDpiAwareBounds { get; private set; }
public Monitor(string monitorName, string monitorInstanceId, int number, int dpi, Rect dpiAwareBounds, Rect dpiUnawareBounds)
{
MonitorName = monitorName;
MonitorInstanceId = monitorInstanceId;
MonitorNumber = number;
Dpi = dpi;
MonitorDpiAwareBounds = dpiAwareBounds;
MonitorDpiUnawareBounds = dpiUnawareBounds;
}
}
}

View File

@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.Windows;
using System.Windows.Media.Imaging;
namespace ProjectsEditor.Models
{
public class MonitorSetup : Monitor, INotifyPropertyChanged
{
private BitmapImage _previewImage;
public BitmapImage PreviewImage
{
get
{
return _previewImage;
}
set
{
_previewImage = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(PreviewImage)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChanged?.Invoke(this, e);
}
public string MonitorInfo { get => MonitorName; }
public string MonitorInfoWithResolution { get => $"{MonitorName} {MonitorDpiAwareBounds.Width}x{MonitorDpiAwareBounds.Height}"; }
public MonitorSetup(string monitorName, string monitorInstanceId, int number, int dpi, Rect dpiAwareBounds, Rect dpiUnawareBounds)
: base(monitorName, monitorInstanceId, number, dpi, dpiAwareBounds, dpiUnawareBounds)
{
}
}
}

View File

@@ -0,0 +1,545 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
namespace ProjectsEditor.Models
{
public class Project : INotifyPropertyChanged
{
public class ScreenHeader : Application
{
public string Title { get; set; }
}
[JsonIgnore]
public string EditorWindowTitle { get; set; }
public string Id { get; set; }
private string _name;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Name)));
}
}
public long CreationTime { get; set; } // in seconds
public long LastLaunchedTime { get; set; } // in seconds
public bool IsShortcutNeeded { get; set; }
public string LastLaunched
{
get
{
string lastLaunched = ProjectsEditor.Properties.Resources.LastLaunched + ": ";
if (LastLaunchedTime == 0)
{
return lastLaunched + ProjectsEditor.Properties.Resources.Never;
}
const int SECOND = 1;
const int MINUTE = 60 * SECOND;
const int HOUR = 60 * MINUTE;
const int DAY = 24 * HOUR;
const int MONTH = 30 * DAY;
DateTime lastLaunchDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(LastLaunchedTime);
var now = DateTime.UtcNow.Ticks;
var ts = DateTime.UtcNow - lastLaunchDateTime;
double delta = Math.Abs(ts.TotalSeconds);
if (delta < 1 * MINUTE)
{
return lastLaunched + ProjectsEditor.Properties.Resources.Recently;
}
if (delta < 2 * MINUTE)
{
return lastLaunched + ProjectsEditor.Properties.Resources.OneMinuteAgo;
}
if (delta < 45 * MINUTE)
{
return lastLaunched + ts.Minutes + " " + ProjectsEditor.Properties.Resources.MinutesAgo;
}
if (delta < 90 * MINUTE)
{
return lastLaunched + ProjectsEditor.Properties.Resources.OneHourAgo;
}
if (delta < 24 * HOUR)
{
return lastLaunched + ts.Hours + " " + ProjectsEditor.Properties.Resources.HoursAgo;
}
if (delta < 48 * HOUR)
{
return lastLaunched + ProjectsEditor.Properties.Resources.Yesterday;
}
if (delta < 30 * DAY)
{
return lastLaunched + ts.Days + " " + ProjectsEditor.Properties.Resources.DaysAgo;
}
if (delta < 12 * MONTH)
{
int months = Convert.ToInt32(Math.Floor((double)ts.Days / 30));
return lastLaunched + (months <= 1 ? ProjectsEditor.Properties.Resources.OneMonthAgo : months + " " + ProjectsEditor.Properties.Resources.MonthsAgo);
}
else
{
int years = Convert.ToInt32(Math.Floor((double)ts.Days / 365));
return lastLaunched + (years <= 1 ? ProjectsEditor.Properties.Resources.OneYearAgo : years + " " + ProjectsEditor.Properties.Resources.YearsAgo);
}
}
}
private bool _isPopoupVisible;
[JsonIgnore]
public bool IsPopupVisible
{
get
{
return _isPopoupVisible;
}
set
{
_isPopoupVisible = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsPopupVisible)));
}
}
public List<Application> Applications { get; set; }
public List<object> ApplicationsListed
{
get
{
List<object> applicationsListed = new List<object>();
ILookup<MonitorSetup, Application> apps = Applications.Where(x => !x.Minimized).ToLookup(x => x.MonitorSetup);
foreach (var appItem in apps.OrderBy(x => x.Key.MonitorDpiUnawareBounds.Left).ThenBy(x => x.Key.MonitorDpiUnawareBounds.Top))
{
applicationsListed.Add(appItem.Key.MonitorInfo);
foreach (Application app in appItem)
{
applicationsListed.Add(app);
}
}
var minimizedApps = Applications.Where(x => x.Minimized);
if (minimizedApps.Any())
{
applicationsListed.Add("Minimized Apps");
foreach (Application app in minimizedApps)
{
applicationsListed.Add(app);
}
}
return applicationsListed;
}
}
[JsonIgnore]
public string AppsCountString
{
get
{
return Applications.Where(x => x.IsSelected).Count().ToString(CultureInfo.InvariantCulture) + " apps";
}
}
public List<MonitorSetup> Monitors { get; set; }
private BitmapImage _previewImage;
public Project(Project selectedProjeсt)
{
Name = selectedProjeсt.Name;
PreviewImage = selectedProjeсt.PreviewImage;
IsShortcutNeeded = selectedProjeсt.IsShortcutNeeded;
int screenIndex = 1;
Monitors = new List<MonitorSetup>();
foreach (var item in selectedProjeсt.Monitors.OrderBy(x => x.MonitorDpiAwareBounds.Left).ThenBy(x => x.MonitorDpiAwareBounds.Top))
{
Monitors.Add(new MonitorSetup($"Screen {screenIndex}", item.MonitorInstanceId, item.MonitorNumber, item.Dpi, item.MonitorDpiAwareBounds, item.MonitorDpiUnawareBounds) { PreviewImage = item.PreviewImage });
screenIndex++;
}
Applications = new List<Application>();
foreach (var item in selectedProjeсt.Applications)
{
Applications.Add(new Application()
{
Hwnd = item.Hwnd,
AppPath = item.AppPath,
AppTitle = item.AppTitle,
CommandLineArguments = item.CommandLineArguments,
Minimized = item.Minimized,
Maximized = item.Maximized,
IsSelected = item.IsSelected,
MonitorNumber = item.MonitorNumber,
Position = new Application.WindowPosition() { X = item.Position.X, Y = item.Position.Y, Height = item.Position.Height, Width = item.Position.Width },
Parent = this,
});
}
}
public Project()
{
}
public BitmapImage PreviewImage
{
get
{
return _previewImage;
}
set
{
_previewImage = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(PreviewImage)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChanged?.Invoke(this, e);
}
public async void Initialize()
{
PreviewImage = await Task.Run(() => DrawPreviewIcons());
foreach (MonitorSetup monitor in Monitors)
{
System.Windows.Rect rect = monitor.MonitorDpiAwareBounds;
monitor.PreviewImage = await Task.Run(() => DrawPreview(new Rectangle((int)rect.Left, (int)rect.Top, (int)(rect.Right - rect.Left), (int)(rect.Bottom - rect.Top))));
}
}
private BitmapImage DrawPreviewIcons()
{
var selectedApps = Applications.Where(x => x.IsSelected);
int appsCount = selectedApps.Count();
if (appsCount == 0)
{
return null;
}
Bitmap previewBitmap = new Bitmap(32 * appsCount, 24);
using (Graphics graphics = Graphics.FromImage(previewBitmap))
{
graphics.SmoothingMode = SmoothingMode.AntiAlias;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
int appIndex = 0;
foreach (var app in selectedApps)
{
try
{
graphics.DrawIcon(app.Icon, new Rectangle(32 * appIndex, 0, 24, 24));
}
catch (Exception)
{
}
appIndex++;
}
}
using (var memory = new MemoryStream())
{
previewBitmap.Save(memory, ImageFormat.Png);
memory.Position = 0;
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memory;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.Freeze();
return bitmapImage;
}
}
private BitmapImage DrawPreview(Rectangle bounds)
{
double scale = 0.1;
int Scaled(double value)
{
return (int)(value * scale);
}
Dictionary<string, int> repeatCounter = new Dictionary<string, int>();
var selectedApps = Applications.Where(x => x.IsSelected);
foreach (Application app in selectedApps)
{
if (repeatCounter.TryGetValue(app.AppPath, out int value))
{
repeatCounter[app.AppPath] = ++value;
}
else
{
repeatCounter.Add(app.AppPath, 1);
}
app.RepeatIndex = repeatCounter[app.AppPath];
}
// remove those repeatIndexes, which are single 1-es (no repetions) by setting them to 0
foreach (Application app in selectedApps.Where(x => repeatCounter[x.AppPath] == 1))
{
app.RepeatIndex = 0;
}
foreach (Application app in Applications.Where(x => !x.IsSelected))
{
app.RepeatIndex = 0;
}
// now that all repeat index values are set, update the repeat index strings on UI
foreach (Application app in Applications)
{
app.OnPropertyChanged(new PropertyChangedEventArgs("RepeatIndexString"));
}
Bitmap previewBitmap = new Bitmap(Scaled(bounds.Width), Scaled(bounds.Height * 1.2));
using (Graphics g = Graphics.FromImage(previewBitmap))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.Clear(Color.FromArgb(0, 0, 0, 0));
Brush brush = new SolidBrush(Color.FromArgb(10, 255, 255, 255)); // TODO: set theme-related colors
foreach (Application app in Applications.Where(x => x.IsSelected && !x.Minimized))
{
Rectangle rect = new Rectangle(Scaled(app.ScaledPosition.X - bounds.Left), Scaled(app.ScaledPosition.Y - bounds.Top), Scaled(app.ScaledPosition.Width), Scaled(app.ScaledPosition.Height));
DrawWindow(g, brush, rect, app);
}
Rectangle rectMinimized = new Rectangle(0, Scaled(bounds.Height), Scaled(bounds.Width), Scaled(bounds.Height * 0.2));
DrawWindow(g, brush, rectMinimized, Applications.Where(x => x.IsSelected && x.Minimized));
}
using (var memory = new MemoryStream())
{
previewBitmap.Save(memory, ImageFormat.Png);
memory.Position = 0;
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memory;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.Freeze();
return bitmapImage;
}
}
private Rectangle GetCommonBounds()
{
double minX = Monitors.First().MonitorDpiUnawareBounds.Left;
double minY = Monitors.First().MonitorDpiUnawareBounds.Top;
double maxX = Monitors.First().MonitorDpiUnawareBounds.Right;
double maxY = Monitors.First().MonitorDpiUnawareBounds.Bottom;
foreach (var monitor in Monitors)
{
minX = Math.Min(minX, monitor.MonitorDpiUnawareBounds.Left);
minY = Math.Min(minY, monitor.MonitorDpiUnawareBounds.Top);
maxX = Math.Max(maxX, monitor.MonitorDpiUnawareBounds.Right);
maxY = Math.Max(maxY, monitor.MonitorDpiUnawareBounds.Bottom);
}
return new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY));
}
public static GraphicsPath RoundedRect(Rectangle bounds)
{
int minorSize = Math.Min(bounds.Width, bounds.Height);
int radius = (int)(minorSize / 8);
int diameter = radius * 2;
Size size = new Size(diameter, diameter);
Rectangle arc = new Rectangle(bounds.Location, size);
GraphicsPath path = new GraphicsPath();
if (radius == 0)
{
path.AddRectangle(bounds);
return path;
}
// top left arc
path.AddArc(arc, 180, 90);
// top right arc
arc.X = bounds.Right - diameter;
path.AddArc(arc, 270, 90);
// bottom right arc
arc.Y = bounds.Bottom - diameter;
path.AddArc(arc, 0, 90);
// bottom left arc
arc.X = bounds.Left;
path.AddArc(arc, 90, 90);
path.CloseFigure();
return path;
}
public static void DrawWindow(Graphics graphics, Brush brush, Rectangle bounds, Application app)
{
if (graphics == null)
{
return;
}
if (brush == null)
{
return;
}
using (GraphicsPath path = RoundedRect(bounds))
{
if (app.IsHighlighted)
{
graphics.DrawPath(new Pen(Color.White, graphics.VisibleClipBounds.Height / 25), path); // TODO: set theme-related colors
}
else
{
graphics.DrawPath(new Pen(Color.FromArgb(128, 82, 82, 82), graphics.VisibleClipBounds.Height / 100), path); // TODO: set theme-related colors
}
graphics.FillPath(brush, path);
}
double iconSize = Math.Min(bounds.Width, bounds.Height) * 0.3;
Rectangle iconBounds = new Rectangle((int)(bounds.Left + (bounds.Width / 2) - (iconSize / 2)), (int)(bounds.Top + (bounds.Height / 2) - (iconSize / 2)), (int)iconSize, (int)iconSize);
try
{
graphics.DrawIcon(app.Icon, iconBounds);
if (app.RepeatIndex > 0)
{
string indexString = app.RepeatIndex.ToString(CultureInfo.InvariantCulture);
System.Drawing.Font font = new System.Drawing.Font("Tahoma", 8);
int indexSize = (int)(iconBounds.Width * 0.5);
Rectangle indexBounds = new Rectangle(iconBounds.Right - indexSize, iconBounds.Bottom - indexSize, indexSize, indexSize);
var textSize = graphics.MeasureString(indexString, font);
var state = graphics.Save();
graphics.TranslateTransform(indexBounds.Left, indexBounds.Top);
graphics.ScaleTransform(indexBounds.Width / textSize.Width, indexBounds.Height / textSize.Height);
graphics.DrawString(indexString, font, Brushes.Black, PointF.Empty);
graphics.Restore(state);
}
}
catch (Exception)
{
// sometimes drawing an icon throws an exception despite that the icon seems to be ok
}
}
public static void DrawWindow(Graphics graphics, Brush brush, Rectangle bounds, IEnumerable<Application> apps)
{
int appsCount = apps.Count();
if (appsCount == 0)
{
return;
}
if (graphics == null)
{
return;
}
if (brush == null)
{
return;
}
using (GraphicsPath path = RoundedRect(bounds))
{
if (apps.Where(x => x.IsHighlighted).Any())
{
graphics.DrawPath(new Pen(Color.White, graphics.VisibleClipBounds.Height / 25), path);
}
else
{
graphics.DrawPath(new Pen(Color.FromArgb(128, 82, 82, 82), graphics.VisibleClipBounds.Height / 100), path);
}
graphics.FillPath(brush, path);
}
double iconSize = Math.Min(bounds.Width, bounds.Height) * 0.5;
for (int iconCounter = 0; iconCounter < appsCount; iconCounter++)
{
Application app = apps.ElementAt(iconCounter);
Rectangle iconBounds = new Rectangle((int)(bounds.Left + (bounds.Width / 2) - (iconSize * ((appsCount / 2) - iconCounter))), (int)(bounds.Top + (bounds.Height / 2) - (iconSize / 2)), (int)iconSize, (int)iconSize);
try
{
graphics.DrawIcon(app.Icon, iconBounds);
if (app.RepeatIndex > 0)
{
string indexString = app.RepeatIndex.ToString(CultureInfo.InvariantCulture);
System.Drawing.Font font = new System.Drawing.Font("Tahoma", 8);
int indexSize = (int)(iconBounds.Width * 0.5);
Rectangle indexBounds = new Rectangle(iconBounds.Right - indexSize, iconBounds.Bottom - indexSize, indexSize, indexSize);
var textSize = graphics.MeasureString(indexString, font);
var state = graphics.Save();
graphics.TranslateTransform(indexBounds.Left, indexBounds.Top);
graphics.ScaleTransform(indexBounds.Width / textSize.Width, indexBounds.Height / textSize.Height);
graphics.DrawString(indexString, font, Brushes.Black, PointF.Empty);
graphics.Restore(state);
}
}
catch (Exception)
{
// sometimes drawing an icon throws an exception despite that the icon seems to be ok
}
}
}
}
}

View File

@@ -0,0 +1,317 @@
<Page x:Class="ProjectsEditor.ProjectEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:props="clr-namespace:ProjectsEditor.Properties"
xmlns:local="clr-namespace:ProjectsEditor"
xmlns:models="clr-namespace:ProjectsEditor.Models"
mc:Ignorable="d"
Title="Project Editor"
Background="{DynamicResource PrimaryBackgroundBrush}">
<Page.Resources>
<Style TargetType="{x:Type CheckBox}">
<Setter Property="Background" Value="{DynamicResource TertiaryBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource SecondaryBorderBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource SecondaryForegroundBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch" >
<Border BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" Width="15" Height="15">
<Grid>
<Grid Background="{TemplateBinding Foreground}" Margin="1" Visibility="Collapsed" Name="nullBlock"/>
<Path Stretch="Uniform" Width="15" Height="10" Fill="{TemplateBinding Foreground}" Name="eliCheck" Data="F1 M 9.97498,1.22334L 4.6983,9.09834L 4.52164,9.09834L 0,5.19331L 1.27664,3.52165L 4.255,6.08833L 8.33331,1.52588e-005L 9.97498,1.22334 Z " Visibility="Collapsed"/>
</Grid>
</Border>
<TextBlock Margin="5,0,0,0" VerticalAlignment="Center" Foreground="{DynamicResource PrimaryForegroundBrush}" Text="{TemplateBinding Content}"></TextBlock>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryBorderBrush}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryForegroundBrush}" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="eliCheck" Property="Visibility" Value="Visible"></Setter>
</Trigger>
<Trigger Property="IsChecked" Value="{x:Null}">
<Setter TargetName="nullBlock" Property="Visibility" Value="Visible"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="CancelButtonStyle" TargetType="Button">
<Setter Property="Background" Value="#9d0808" />
<Setter Property="BorderBrush" Value="#FF262E34"/>
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}" BorderBrush="#FF262E34" BorderThickness="1">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#ec4d37"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#9d0808"/>
</Trigger>
</Style.Triggers>
</Style>
<DataTemplate x:Key="headerTemplate">
<Border>
<TextBlock
Text="{Binding .}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
FontSize="14"
FontWeight="Normal"
Margin="0,20,20,5"
VerticalAlignment="Center"/>
</Border>
</DataTemplate>
<DataTemplate x:Key="appTemplate">
<Border
Background="{DynamicResource SecondaryBackgroundBrush}"
MouseEnter="AppBorder_MouseEnter"
MouseLeave="AppBorder_MouseLeave">
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Image
Width="20"
Height="20"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="10"
Source="{Binding IconBitmapImage}"/>
<TextBlock
Grid.Column="1"
Text="{Binding RepeatIndexString, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
FontSize="14"
FontWeight="Normal"
Width="20"
VerticalAlignment="Center"/>
<TextBlock
Grid.Column="2"
Text="{Binding AppName}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
FontSize="14"
FontWeight="Normal"
VerticalAlignment="Center"/>
<TextBox
x:Name="CommandLineTextBox"
Grid.Column="3"
Text="{Binding CommandLineArguments, Mode=TwoWay}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Background="{DynamicResource TertiaryBackgroundBrush}"
BorderThickness="0"
FontSize="14"
FontWeight="Normal"
VerticalContentAlignment="Center" />
<TextBlock
Grid.Column="3"
IsHitTestVisible="False"
Text="{x:Static props:Resources.WriteArgs}"
Foreground="{DynamicResource SecondaryForegroundBrush}"
Background="{DynamicResource TertiaryBackgroundBrush}"
FontSize="14"
FontWeight="Normal"
VerticalAlignment="Center"
Margin="12,0,12,0">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=CommandLineTextBox}" Value="">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<CheckBox
Grid.Column="4"
IsChecked="{Binding IsSelected, Mode=TwoWay}"
Checked="CheckBox_Checked"
Unchecked="CheckBox_Checked"
Margin="10"/>
</Grid>
</Border>
</DataTemplate>
<models:AppListDataTemplateSelector
HeaderTemplate="{StaticResource headerTemplate}"
AppTemplate="{StaticResource appTemplate}"
x:Key="AppListDataTemplateSelector"/>
</Page.Resources>
<Grid Margin="40,0,40,40">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock Text="{x:Static props:Resources.Projects}" FontSize="24" FontWeight="Normal" Margin="0,20,0,20" Foreground="{DynamicResource PrimaryForegroundBrush}"/>
<TextBlock
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource PrimaryForegroundBrush}"
FontSize="16"
Margin="10,30,0,20"
Text="&#xE76C;" />
<TextBlock Text="{Binding EditorWindowTitle}" FontSize="24" FontWeight="SemiBold" Margin="10,20,0,20" Foreground="{DynamicResource PrimaryForegroundBrush}"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Vertical">
<TextBlock Text="{x:Static props:Resources.ProjectName}" FontSize="14" FontWeight="Normal" Foreground="{DynamicResource PrimaryForegroundBrush}"/>
<TextBox
x:Name="EditNameTextBox"
Width="320"
Text="{Binding Name, Mode=TwoWay}"
Background="{DynamicResource SecondaryBackgroundBrush}"
BorderBrush="{DynamicResource PrimaryBorderBrush}"
BorderThickness="2"
Margin="0,6,0,6"
HorizontalAlignment="Left"
GotFocus="EditNameTextBox_GotFocus"
KeyDown="EditNameTextBoxKeyDown" />
</StackPanel>
<ScrollViewer
VerticalScrollBarVisibility="Auto"
Grid.Row="2">
<StackPanel Orientation="Vertical">
<Border
Grid.Row="2"
HorizontalAlignment="Stretch"
Background="{DynamicResource MonitorViewBackgroundBrush}"
CornerRadius="5">
<ScrollViewer
HorizontalAlignment="Center"
HorizontalScrollBarVisibility="Auto">
<ItemsControl
ItemsSource="{Binding Monitors, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
IsItemsHost="True"
Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="models:MonitorSetup">
<Grid
Margin="20,20,20,20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding MonitorInfoWithResolution}" Foreground="{DynamicResource PrimaryForegroundBrush}" FontSize="16" FontWeight="Normal" Margin="0,5,0,5"/>
<Border
Grid.Row="1"
CornerRadius="5"
BorderBrush="{DynamicResource TertiaryBackgroundBrush}"
BorderThickness="2"
Margin="0,0,0,10">
<Image
Width="200"
Height="140"
Source="{Binding PreviewImage, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Stretch="Fill"
Margin="2"/>
</Border>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ItemsControl
ItemsSource="{Binding ApplicationsListed, Mode=OneWay}"
ItemTemplateSelector="{StaticResource AppListDataTemplateSelector}">
</ItemsControl>
</Grid>
</StackPanel>
</ScrollViewer>
<DockPanel Grid.Row="3" Margin="0,20,0,20">
<CheckBox
DockPanel.Dock="Left"
Content="{x:Static props:Resources.CreateShortcut}"
IsChecked="{Binding IsShortcutNeeded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
FontSize="14"/>
<StackPanel
DockPanel.Dock="Right"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="40,0,0,0">
<Button
x:Name="CancelButton"
Margin="20,0,0,0"
Height="36"
AutomationProperties.Name="{x:Static props:Resources.Cancel}"
Click="CancelButtonClicked">
<StackPanel Orientation="Horizontal" Margin="12, 2, 12, 0" >
<TextBlock
AutomationProperties.Name="{x:Static props:Resources.Cancel}"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource AccentButtonForeground}"
Text="&#xE711;" />
<TextBlock
Margin="12,-4,0,0"
Foreground="{DynamicResource AccentButtonForeground}"
Text="{x:Static props:Resources.Cancel}" />
</StackPanel>
<Button.Effect>
<DropShadowEffect
BlurRadius="6"
Opacity="0.32"
ShadowDepth="1" />
</Button.Effect>
</Button>
<Button
x:Name="SaveButton"
Margin="20,0,0,0"
Height="36"
AutomationProperties.Name="{x:Static props:Resources.Save_project}"
Click="SaveButtonClicked"
Style="{StaticResource AccentButtonStyle}">
<StackPanel Orientation="Horizontal" Margin="12, 2, 12, 0" >
<TextBlock
AutomationProperties.Name="{x:Static props:Resources.Save_project}"
FontFamily="{DynamicResource SymbolThemeFontFamily}"
Foreground="{DynamicResource AccentButtonForeground}"
Text="&#xE74E;" />
<TextBlock
Margin="12,-4,0,0"
Foreground="{DynamicResource AccentButtonForeground}"
Text="{x:Static props:Resources.Save_project}" />
</StackPanel>
<Button.Effect>
<DropShadowEffect
BlurRadius="6"
Opacity="0.32"
ShadowDepth="1" />
</Button.Effect>
</Button>
</StackPanel>
</DockPanel>
</Grid>
</Page>

View File

@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using ProjectsEditor.Models;
using ProjectsEditor.ViewModels;
namespace ProjectsEditor
{
/// <summary>
/// Interaction logic for ProjectEditor.xaml
/// </summary>
public partial class ProjectEditor : Page
{
private MainViewModel _mainViewModel;
public ProjectEditor(MainViewModel mainViewModel)
{
_mainViewModel = mainViewModel;
InitializeComponent();
}
private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
CheckBox checkBox = sender as CheckBox;
Models.Application application = checkBox.DataContext as Models.Application;
Models.Project project = application.Parent;
project.Initialize();
}
private void SaveButtonClicked(object sender, RoutedEventArgs e)
{
Project projectToSave = this.DataContext as Project;
_mainViewModel.SaveProject(projectToSave);
_mainViewModel.SwitchToMainView();
}
private void CancelButtonClicked(object sender, RoutedEventArgs e)
{
_mainViewModel.CancelLastEdit();
_mainViewModel.SwitchToMainView();
}
private void EditNameTextBoxKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
e.Handled = true;
Project project = this.DataContext as Project;
project.Name = EditNameTextBox.Text;
}
else if (e.Key == Key.Escape)
{
e.Handled = true;
Project project = this.DataContext as Project;
_mainViewModel.CancelProjectName(project);
}
}
private void EditNameTextBox_GotFocus(object sender, RoutedEventArgs e)
{
_mainViewModel.SaveProjectName(DataContext as Project);
}
private void AppBorder_MouseEnter(object sender, MouseEventArgs e)
{
Border border = sender as Border;
Models.Application app = border.DataContext as Models.Application;
app.IsHighlighted = true;
Project project = app.Parent;
project.Initialize();
}
private void AppBorder_MouseLeave(object sender, MouseEventArgs e)
{
Border border = sender as Border;
Models.Application app = border.DataContext as Models.Application;
app.IsHighlighted = false;
Project project = app.Parent;
project.Initialize();
}
}
}

View File

@@ -0,0 +1,105 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyTitle>ProjectsEditor</AssemblyTitle>
<AssemblyDescription>ProjectsEditor</AssemblyDescription>
<Description>ProjectsEditor</Description>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<OutputPath>..\$(Platform)\$(Configuration)</OutputPath>
<SelfContained>true</SelfContained>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
<!-- SelfContained=true requires RuntimeIdentifier to be set -->
<PropertyGroup Condition="'$(Platform)'=='x64'">
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup Condition="'$(Platform)'=='ARM64'">
<RuntimeIdentifier>win-arm64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup>
<ProjectGuid>{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}</ProjectGuid>
<TargetFramework>net8.0-windows10.0.20348.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<Optimize>true</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<Optimize>false</Optimize>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>images\Projects.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AssemblyName>ProjectsEditor</AssemblyName>
</PropertyGroup>
<ItemGroup>
<None Remove="images\DefaultIcon.ico" />
<None Remove="images\Projects.ico" />
</ItemGroup>
<ItemGroup>
<COMReference Include="IWshRuntimeLibrary">
<WrapperTool>tlbimp</WrapperTool>
<VersionMinor>0</VersionMinor>
<VersionMajor>1</VersionMajor>
<Guid>f935dc20-1cf0-11d0-adb9-00c04fd58a0b</Guid>
<Lcid>0</Lcid>
<Isolated>false</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup>
<Content Include="images\DefaultIcon.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="images\Projects.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="app.manifest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ControlzEx" />
<PackageReference Include="Microsoft.Windows.CsWinRT" />
<PackageReference Include="ModernWpfUI" />
<PackageReference Include="System.IO.Abstractions" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,432 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace ProjectsEditor.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ProjectsEditor.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to App name.
/// </summary>
public static string App_name {
get {
return ResourceManager.GetString("App_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Are you sure?.
/// </summary>
public static string Are_You_Sure {
get {
return ResourceManager.GetString("Are_You_Sure", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Are you sure you want to delete this project?.
/// </summary>
public static string Are_You_Sure_Description {
get {
return ResourceManager.GetString("Are_You_Sure_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Cancel.
/// </summary>
public static string Cancel {
get {
return ResourceManager.GetString("Cancel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Created.
/// </summary>
public static string Created {
get {
return ResourceManager.GetString("Created", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Create project.
/// </summary>
public static string CreateProject {
get {
return ResourceManager.GetString("CreateProject", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Create Shortcut.
/// </summary>
public static string CreateShortcut {
get {
return ResourceManager.GetString("CreateShortcut", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to days ago.
/// </summary>
public static string DaysAgo {
get {
return ResourceManager.GetString("DaysAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remove.
/// </summary>
public static string Delete {
get {
return ResourceManager.GetString("Delete", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delete project dialog..
/// </summary>
public static string Delete_Project_Dialog_Announce {
get {
return ResourceManager.GetString("Delete_Project_Dialog_Announce", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Edit.
/// </summary>
public static string Edit {
get {
return ResourceManager.GetString("Edit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to opened.
/// </summary>
public static string Edit_Project_Open_Announce {
get {
return ResourceManager.GetString("Edit_Project_Open_Announce", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Edit project.
/// </summary>
public static string EditProject {
get {
return ResourceManager.GetString("EditProject", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Error parsing projects data..
/// </summary>
public static string Error_Parsing_Message {
get {
return ResourceManager.GetString("Error_Parsing_Message", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to hours ago.
/// </summary>
public static string HoursAgo {
get {
return ResourceManager.GetString("HoursAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Last launched.
/// </summary>
public static string LastLaunched {
get {
return ResourceManager.GetString("LastLaunched", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch.
/// </summary>
public static string Launch {
get {
return ResourceManager.GetString("Launch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch args.
/// </summary>
public static string Launch_args {
get {
return ResourceManager.GetString("Launch_args", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Projects demo app.
/// </summary>
public static string MainTitle {
get {
return ResourceManager.GetString("MainTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to minutes ago.
/// </summary>
public static string MinutesAgo {
get {
return ResourceManager.GetString("MinutesAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to months ago.
/// </summary>
public static string MonthsAgo {
get {
return ResourceManager.GetString("MonthsAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name.
/// </summary>
public static string Name {
get {
return ResourceManager.GetString("Name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to never.
/// </summary>
public static string Never {
get {
return ResourceManager.GetString("Never", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to New project.
/// </summary>
public static string New_project {
get {
return ResourceManager.GetString("New_project", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to There are no saved projects..
/// </summary>
public static string No_Projects_Message {
get {
return ResourceManager.GetString("No_Projects_Message", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to an hour ago.
/// </summary>
public static string OneHourAgo {
get {
return ResourceManager.GetString("OneHourAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to a minute ago.
/// </summary>
public static string OneMinuteAgo {
get {
return ResourceManager.GetString("OneMinuteAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to one month ago.
/// </summary>
public static string OneMonthAgo {
get {
return ResourceManager.GetString("OneMonthAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to one second ago.
/// </summary>
public static string OneSecondAgo {
get {
return ResourceManager.GetString("OneSecondAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to one year ago.
/// </summary>
public static string OneYearAgo {
get {
return ResourceManager.GetString("OneYearAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Pin Project to Taskbar.
/// </summary>
public static string PinToTaskbar {
get {
return ResourceManager.GetString("PinToTaskbar", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name project.
/// </summary>
public static string ProjectName {
get {
return ResourceManager.GetString("ProjectName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Projects.
/// </summary>
public static string Projects {
get {
return ResourceManager.GetString("Projects", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to recently.
/// </summary>
public static string Recently {
get {
return ResourceManager.GetString("Recently", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save project.
/// </summary>
public static string Save_project {
get {
return ResourceManager.GetString("Save_project", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search.
/// </summary>
public static string Search {
get {
return ResourceManager.GetString("Search", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to seconds ago.
/// </summary>
public static string SecondsAgo {
get {
return ResourceManager.GetString("SecondsAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Sort by.
/// </summary>
public static string SortBy {
get {
return ResourceManager.GetString("SortBy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Write arguments here.
/// </summary>
public static string WriteArgs {
get {
return ResourceManager.GetString("WriteArgs", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to years ago.
/// </summary>
public static string YearsAgo {
get {
return ResourceManager.GetString("YearsAgo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to yesterday.
/// </summary>
public static string Yesterday {
get {
return ResourceManager.GetString("Yesterday", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,243 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="App_name" xml:space="preserve">
<value>App name</value>
</data>
<data name="Are_You_Sure" xml:space="preserve">
<value>Are you sure?</value>
</data>
<data name="Are_You_Sure_Description" xml:space="preserve">
<value>Are you sure you want to delete this project?</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="Created" xml:space="preserve">
<value>Created</value>
</data>
<data name="CreateProject" xml:space="preserve">
<value>Create project</value>
</data>
<data name="CreateShortcut" xml:space="preserve">
<value>Create Shortcut</value>
</data>
<data name="DaysAgo" xml:space="preserve">
<value>days ago</value>
</data>
<data name="Delete" xml:space="preserve">
<value>Remove</value>
</data>
<data name="Delete_Project_Dialog_Announce" xml:space="preserve">
<value>Delete project dialog.</value>
</data>
<data name="Edit" xml:space="preserve">
<value>Edit</value>
</data>
<data name="EditProject" xml:space="preserve">
<value>Edit project</value>
</data>
<data name="Edit_Project_Open_Announce" xml:space="preserve">
<value>opened</value>
</data>
<data name="Error_Parsing_Message" xml:space="preserve">
<value>Error parsing projects data.</value>
</data>
<data name="HoursAgo" xml:space="preserve">
<value>hours ago</value>
</data>
<data name="LastLaunched" xml:space="preserve">
<value>Last launched</value>
</data>
<data name="Launch" xml:space="preserve">
<value>Launch</value>
</data>
<data name="Launch_args" xml:space="preserve">
<value>Launch args</value>
</data>
<data name="MainTitle" xml:space="preserve">
<value>Projects demo app</value>
</data>
<data name="MinutesAgo" xml:space="preserve">
<value>minutes ago</value>
</data>
<data name="MonthsAgo" xml:space="preserve">
<value>months ago</value>
</data>
<data name="Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="Never" xml:space="preserve">
<value>never</value>
</data>
<data name="New_project" xml:space="preserve">
<value>New project</value>
</data>
<data name="No_Projects_Message" xml:space="preserve">
<value>There are no saved projects.</value>
</data>
<data name="OneHourAgo" xml:space="preserve">
<value>an hour ago</value>
</data>
<data name="OneMinuteAgo" xml:space="preserve">
<value>a minute ago</value>
</data>
<data name="OneMonthAgo" xml:space="preserve">
<value>one month ago</value>
</data>
<data name="OneSecondAgo" xml:space="preserve">
<value>one second ago</value>
</data>
<data name="OneYearAgo" xml:space="preserve">
<value>one year ago</value>
</data>
<data name="PinToTaskbar" xml:space="preserve">
<value>Pin Project to Taskbar</value>
</data>
<data name="ProjectName" xml:space="preserve">
<value>Name project</value>
</data>
<data name="Projects" xml:space="preserve">
<value>Projects</value>
</data>
<data name="Recently" xml:space="preserve">
<value>recently</value>
</data>
<data name="Save_project" xml:space="preserve">
<value>Save project</value>
</data>
<data name="Search" xml:space="preserve">
<value>Search</value>
</data>
<data name="SecondsAgo" xml:space="preserve">
<value>seconds ago</value>
</data>
<data name="SortBy" xml:space="preserve">
<value>Sort by</value>
</data>
<data name="WriteArgs" xml:space="preserve">
<value>Write arguments here</value>
</data>
<data name="YearsAgo" xml:space="preserve">
<value>years ago</value>
</data>
<data name="Yesterday" xml:space="preserve">
<value>yesterday</value>
</data>
</root>

View File

@@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace ProjectsEditor.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.1.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
}
}

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View File

@@ -0,0 +1,55 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.modernwpf.com/2019">
<Style
x:Key="IconOnlyButtonStyle"
BasedOn="{StaticResource DefaultButtonStyle}"
TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
x:Name="Background"
Background="Transparent"
CornerRadius="{TemplateBinding ui:ControlHelper.CornerRadius}"
SnapsToDevicePixels="True">
<Border
x:Name="Border"
Padding="{TemplateBinding Padding}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding ui:ControlHelper.CornerRadius}">
<ContentPresenter
x:Name="ContentPresenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Focusable="False"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Background" Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" />
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPointerOver}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Background" Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Background" Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
<Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,25 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<!-- Metadata -->
<system:String x:Key="Theme.Name">Dark.Accent1</system:String>
<system:String x:Key="Theme.Origin">Origin</system:String>
<system:String x:Key="Theme.DisplayName">Accent1 (Dark)</system:String>
<system:String x:Key="Theme.BaseColorScheme">Dark</system:String>
<system:String x:Key="Theme.ColorScheme">Accent1</system:String>
<Color x:Key="Theme.PrimaryAccentColor">Black</Color>
<SolidColorBrush x:Key="PrimaryBackgroundBrush" Color="#FF242424" />
<SolidColorBrush x:Key="SecondaryBackgroundBrush" Color="#FF2b2b2b" />
<SolidColorBrush x:Key="TertiaryBackgroundBrush" Color="#FF373737" />
<SolidColorBrush x:Key="MonitorViewBackgroundBrush" Color="#FF161616" />
<SolidColorBrush x:Key="PrimaryForegroundBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="SecondaryForegroundBrush" Color="#FF9a9a9a" />
<SolidColorBrush x:Key="PrimaryBorderBrush" Color="#FF373737" />
<SolidColorBrush x:Key="SecondaryBorderBrush" Color="#FF494949" />
<SolidColorBrush x:Key="TertiaryBorderBrush" Color="#FF202020" />
<SolidColorBrush x:Key="BackdropBrush" Color="#40F0F0F0" />
<SolidColorBrush x:Key="TitleBarSecondaryForegroundBrush" Color="#FF9a9a9a" />
</ResourceDictionary>

View File

@@ -0,0 +1,25 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<!-- Metadata -->
<system:String x:Key="Theme.Name">HighContrast.Accent2</system:String>
<system:String x:Key="Theme.Origin">Origin</system:String>
<system:String x:Key="Theme.DisplayName">Accent2 (HighContrast)</system:String>
<system:String x:Key="Theme.BaseColorScheme">HighContrast</system:String>
<system:String x:Key="Theme.ColorScheme">Accent2</system:String>
<Color x:Key="Theme.PrimaryAccentColor">White</Color>
<SolidColorBrush x:Key="PrimaryBackgroundBrush" Color="#FF242424" />
<SolidColorBrush x:Key="SecondaryBackgroundBrush" Color="#FF1c1c1c" />
<SolidColorBrush x:Key="TertiaryBackgroundBrush" Color="#FF202020" />
<SolidColorBrush x:Key="MonitorViewBackgroundBrush" Color="#FF161616" />
<SolidColorBrush x:Key="PrimaryForegroundBrush" Color="#FFffff00" />
<SolidColorBrush x:Key="SecondaryForegroundBrush" Color="#FF00ff00" />
<SolidColorBrush x:Key="PrimaryBorderBrush" Color="#FF373737" />
<SolidColorBrush x:Key="SecondaryBorderBrush" Color="#FF494949" />
<SolidColorBrush x:Key="TertiaryBorderBrush" Color="#FF202020" />
<SolidColorBrush x:Key="BackdropBrush" Color="#E55B5B5B" />
<SolidColorBrush x:Key="TitleBarSecondaryForegroundBrush" Color="#FF9a9a9a" />
</ResourceDictionary>

View File

@@ -0,0 +1,25 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<!-- Metadata -->
<system:String x:Key="Theme.Name">HighContrast.Accent3</system:String>
<system:String x:Key="Theme.Origin">Origin</system:String>
<system:String x:Key="Theme.DisplayName">Accent3 (HighContrast)</system:String>
<system:String x:Key="Theme.BaseColorScheme">HighContrast</system:String>
<system:String x:Key="Theme.ColorScheme">Accent3</system:String>
<Color x:Key="Theme.PrimaryAccentColor">White</Color>
<SolidColorBrush x:Key="PrimaryBackgroundBrush" Color="#FF242424" />
<SolidColorBrush x:Key="SecondaryBackgroundBrush" Color="#FF1c1c1c" />
<SolidColorBrush x:Key="TertiaryBackgroundBrush" Color="#FF202020" />
<SolidColorBrush x:Key="MonitorViewBackgroundBrush" Color="#FF161616" />
<SolidColorBrush x:Key="PrimaryForegroundBrush" Color="#FFffff00" />
<SolidColorBrush x:Key="SecondaryForegroundBrush" Color="#FFc0c0c0" />
<SolidColorBrush x:Key="PrimaryBorderBrush" Color="#FF373737" />
<SolidColorBrush x:Key="SecondaryBorderBrush" Color="#FF494949" />
<SolidColorBrush x:Key="TertiaryBorderBrush" Color="#FF202020" />
<SolidColorBrush x:Key="BackdropBrush" Color="#E55B5B5B" />
<SolidColorBrush x:Key="TitleBarSecondaryForegroundBrush" Color="#FF9a9a9a" />
</ResourceDictionary>

View File

@@ -0,0 +1,25 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<!-- Metadata -->
<system:String x:Key="Theme.Name">HighContrast.Accent4</system:String>
<system:String x:Key="Theme.Origin">Origin</system:String>
<system:String x:Key="Theme.DisplayName">Accent4 (HighContrast)</system:String>
<system:String x:Key="Theme.BaseColorScheme">HighContrast</system:String>
<system:String x:Key="Theme.ColorScheme">Accent4</system:String>
<Color x:Key="Theme.PrimaryAccentColor">White</Color>
<SolidColorBrush x:Key="PrimaryBackgroundBrush" Color="#FF242424" />
<SolidColorBrush x:Key="SecondaryBackgroundBrush" Color="#FF1c1c1c" />
<SolidColorBrush x:Key="TertiaryBackgroundBrush" Color="#FF202020" />
<SolidColorBrush x:Key="MonitorViewBackgroundBrush" Color="#FF161616" />
<SolidColorBrush x:Key="PrimaryForegroundBrush" Color="#FFffffff" />
<SolidColorBrush x:Key="SecondaryForegroundBrush" Color="#FF1aebff" />
<SolidColorBrush x:Key="PrimaryBorderBrush" Color="#FF373737" />
<SolidColorBrush x:Key="SecondaryBorderBrush" Color="#FF494949" />
<SolidColorBrush x:Key="TertiaryBorderBrush" Color="#FF202020" />
<SolidColorBrush x:Key="BackdropBrush" Color="#E55B5B5B" />
<SolidColorBrush x:Key="TitleBarSecondaryForegroundBrush" Color="#FF9a9a9a" />
</ResourceDictionary>

View File

@@ -0,0 +1,25 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<!-- Metadata -->
<system:String x:Key="Theme.Name">HighContrast.Accent5</system:String>
<system:String x:Key="Theme.Origin">Origin</system:String>
<system:String x:Key="Theme.DisplayName">Accent5 (HighContrast)</system:String>
<system:String x:Key="Theme.BaseColorScheme">HighContrast</system:String>
<system:String x:Key="Theme.ColorScheme">Accent5</system:String>
<Color x:Key="Theme.PrimaryAccentColor">White</Color>
<SolidColorBrush x:Key="PrimaryBackgroundBrush" Color="#FFf9f9f9" />
<SolidColorBrush x:Key="SecondaryBackgroundBrush" Color="#FFeeeeee" />
<SolidColorBrush x:Key="TertiaryBackgroundBrush" Color="#FFF3F3F3" />
<SolidColorBrush x:Key="MonitorViewBackgroundBrush" Color="#FF161616" />
<SolidColorBrush x:Key="PrimaryForegroundBrush" Color="#FF000000" />
<SolidColorBrush x:Key="SecondaryForegroundBrush" Color="#FF37006e" />
<SolidColorBrush x:Key="PrimaryBorderBrush" Color="#FF373737" />
<SolidColorBrush x:Key="SecondaryBorderBrush" Color="#FF494949" />
<SolidColorBrush x:Key="TertiaryBorderBrush" Color="#FF202020" />
<SolidColorBrush x:Key="BackdropBrush" Color="#E5949494" />
<SolidColorBrush x:Key="TitleBarSecondaryForegroundBrush" Color="#FF949494" />
</ResourceDictionary>

View File

@@ -0,0 +1,25 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<!-- Metadata -->
<system:String x:Key="Theme.Name">Light.Accent1</system:String>
<system:String x:Key="Theme.Origin">Origin</system:String>
<system:String x:Key="Theme.DisplayName">Accent1 (Light)</system:String>
<system:String x:Key="Theme.BaseColorScheme">Light</system:String>
<system:String x:Key="Theme.ColorScheme">Accent1</system:String>
<Color x:Key="Theme.PrimaryAccentColor">White</Color>
<SolidColorBrush x:Key="PrimaryBackgroundBrush" Color="#FFF9F9F9" />
<SolidColorBrush x:Key="SecondaryBackgroundBrush" Color="#FFeeeeee" />
<SolidColorBrush x:Key="TertiaryBackgroundBrush" Color="#FFF3F3F3" />
<SolidColorBrush x:Key="MonitorViewBackgroundBrush" Color="#FFF9F9F9" />
<SolidColorBrush x:Key="PrimaryForegroundBrush" Color="#FF000000" />
<SolidColorBrush x:Key="SecondaryForegroundBrush" Color="#FF6A6A6A" />
<SolidColorBrush x:Key="PrimaryBorderBrush" Color="#FFe5e5e5" />
<SolidColorBrush x:Key="SecondaryBorderBrush" Color="#FFe5e5e5" />
<SolidColorBrush x:Key="TertiaryBorderBrush" Color="#FFe5e5e5" />
<SolidColorBrush x:Key="BackdropBrush" Color="#85F0F0F0" />
<SolidColorBrush x:Key="TitleBarSecondaryForegroundBrush" Color="#FF949494" />
</ResourceDictionary>

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json;
namespace ProjectsEditor.Utils
{
public class DashCaseNamingPolicy : JsonNamingPolicy
{
public static DashCaseNamingPolicy Instance { get; } = new DashCaseNamingPolicy();
public override string ConvertName(string name)
{
return name.UpperCamelCaseToDashCase();
}
}
}

View File

@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.IO.Abstractions;
using System.Threading.Tasks;
namespace ProjectsEditor.Utils
{
public class IOUtils
{
private readonly IFileSystem _fileSystem = new FileSystem();
public IOUtils()
{
}
public void WriteFile(string fileName, string data)
{
_fileSystem.File.WriteAllText(fileName, data);
}
public string ReadFile(string fileName)
{
if (_fileSystem.File.Exists(fileName))
{
var attempts = 0;
while (attempts < 10)
{
try
{
using (Stream inputStream = _fileSystem.File.Open(fileName, FileMode.Open))
using (StreamReader reader = new StreamReader(inputStream))
{
string data = reader.ReadToEnd();
inputStream.Close();
return data;
}
}
catch (Exception)
{
Task.Delay(10).Wait();
}
attempts++;
}
}
return string.Empty;
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace ProjectsEditor.Utils
{
internal sealed class NativeMethods
{
public const int SW_RESTORE = 9;
public const int SW_NORMAL = 1;
public const int SW_MINIMIZE = 6;
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
[DllImport("USER32.DLL")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
public static extern void SwitchToThisWindow(IntPtr hWnd, bool fAltTab);
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr processId);
[DllImport("kernel32.dll")]
public static extern uint GetCurrentThreadId();
[DllImport("user32.dll")]
public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace ProjectsEditor.Utils
{
public struct ParsingResult
{
public bool Result { get; }
public string Message { get; }
public string MalformedData { get; }
public ParsingResult(bool result, string message = "", string data = "")
{
Result = result;
Message = message;
MalformedData = data;
}
}
}

View File

@@ -0,0 +1,233 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows;
using ProjectsEditor.Data;
using ProjectsEditor.Models;
using ProjectsEditor.ViewModels;
namespace ProjectsEditor.Utils
{
public class ProjectsEditorIO
{
public ProjectsEditorIO()
{
}
public ParsingResult ParseProjects(MainViewModel mainViewModel)
{
try
{
ProjectsData parser = new ProjectsData();
if (!File.Exists(parser.File))
{
return new ParsingResult(true);
}
ProjectsData.ProjectsListWrapper projects = parser.Read(parser.File);
if (!SetProjects(mainViewModel, projects))
{
return new ParsingResult(false, ProjectsEditor.Properties.Resources.Error_Parsing_Message);
}
return new ParsingResult(true);
}
catch (Exception e)
{
return new ParsingResult(false, e.Message);
}
}
public ParsingResult ParseProject(string fileName, out Project project)
{
project = null;
try
{
ProjectsData parser = new ProjectsData();
if (!File.Exists(fileName))
{
return new ParsingResult(true);
}
ProjectsData.ProjectsListWrapper projects = parser.Read(fileName);
if (!ExtractProject(projects, out project))
{
return new ParsingResult(false, ProjectsEditor.Properties.Resources.Error_Parsing_Message);
}
return new ParsingResult(true);
}
catch (Exception e)
{
return new ParsingResult(false, e.Message);
}
}
private bool ExtractProject(ProjectsData.ProjectsListWrapper projects, out Project project)
{
project = null;
if (projects.Projects == null)
{
return false;
}
if (projects.Projects.Count != 1)
{
return false;
}
ProjectsData.ProjectWrapper projectWrapper = projects.Projects[0];
project = GetProjectFromWrappper(projectWrapper);
return true;
}
private Project GetProjectFromWrappper(ProjectsData.ProjectWrapper project)
{
Project newProject = new Project()
{
Id = project.Id,
Name = project.Name,
CreationTime = project.CreationTime,
LastLaunchedTime = project.LastLaunchedTime,
IsShortcutNeeded = project.IsShortcutNeeded,
Monitors = new List<MonitorSetup>() { },
Applications = new List<Models.Application> { },
};
foreach (var app in project.Applications)
{
newProject.Applications.Add(new Models.Application()
{
Hwnd = new IntPtr(app.Hwnd),
AppPath = app.Application,
AppTitle = app.Title,
Parent = newProject,
CommandLineArguments = app.CommandLineArguments,
Maximized = app.Maximized,
Minimized = app.Minimized,
IsSelected = true,
Position = new Models.Application.WindowPosition()
{
Height = app.Position.Height,
Width = app.Position.Width,
X = app.Position.X,
Y = app.Position.Y,
},
MonitorNumber = app.Monitor,
});
}
foreach (var monitor in project.MonitorConfiguration)
{
Rect dpiAware = new Rect(monitor.MonitorRectDpiAware.Left, monitor.MonitorRectDpiAware.Top, monitor.MonitorRectDpiAware.Width, monitor.MonitorRectDpiAware.Height);
Rect dpiUnaware = new Rect(monitor.MonitorRectDpiUnaware.Left, monitor.MonitorRectDpiUnaware.Top, monitor.MonitorRectDpiUnaware.Width, monitor.MonitorRectDpiUnaware.Height);
newProject.Monitors.Add(new MonitorSetup(monitor.Id, monitor.InstanceId, monitor.MonitorNumber, monitor.Dpi, dpiAware, dpiUnaware));
}
return newProject;
}
public void SerializeProjects(List<Project> projects)
{
ProjectsData serializer = new ProjectsData();
ProjectsData.ProjectsListWrapper projectsWrapper = new ProjectsData.ProjectsListWrapper { };
projectsWrapper.Projects = new List<ProjectsData.ProjectWrapper>();
foreach (Project project in projects)
{
ProjectsData.ProjectWrapper wrapper = new ProjectsData.ProjectWrapper
{
Id = project.Id,
Name = project.Name,
CreationTime = project.CreationTime,
IsShortcutNeeded = project.IsShortcutNeeded,
LastLaunchedTime = project.LastLaunchedTime,
Applications = new List<ProjectsData.ApplicationWrapper> { },
MonitorConfiguration = new List<ProjectsData.MonitorConfigurationWrapper> { },
};
foreach (var app in project.Applications)
{
if (app.IsSelected)
{
wrapper.Applications.Add(new ProjectsData.ApplicationWrapper
{
Hwnd = app.Hwnd,
Application = app.AppPath,
Title = app.AppTitle,
CommandLineArguments = app.CommandLineArguments,
Maximized = app.Maximized,
Minimized = app.Minimized,
Position = new ProjectsData.ApplicationWrapper.WindowPositionWrapper
{
X = app.Position.X,
Y = app.Position.Y,
Height = app.Position.Height,
Width = app.Position.Width,
},
Monitor = app.MonitorNumber,
});
}
}
foreach (var monitor in project.Monitors)
{
wrapper.MonitorConfiguration.Add(new ProjectsData.MonitorConfigurationWrapper
{
Id = monitor.MonitorName,
InstanceId = monitor.MonitorInstanceId,
MonitorNumber = monitor.MonitorNumber,
Dpi = monitor.Dpi,
MonitorRectDpiAware = new ProjectsData.MonitorConfigurationWrapper.MonitorRectWrapper
{
Left = (int)monitor.MonitorDpiAwareBounds.Left,
Top = (int)monitor.MonitorDpiAwareBounds.Top,
Width = (int)monitor.MonitorDpiAwareBounds.Width,
Height = (int)monitor.MonitorDpiAwareBounds.Height,
},
MonitorRectDpiUnaware = new ProjectsData.MonitorConfigurationWrapper.MonitorRectWrapper
{
Left = (int)monitor.MonitorDpiUnawareBounds.Left,
Top = (int)monitor.MonitorDpiUnawareBounds.Top,
Width = (int)monitor.MonitorDpiUnawareBounds.Width,
Height = (int)monitor.MonitorDpiUnawareBounds.Height,
},
});
}
projectsWrapper.Projects.Add(wrapper);
}
try
{
IOUtils ioUtils = new IOUtils();
ioUtils.WriteFile(serializer.File, serializer.Serialize(projectsWrapper));
}
catch (Exception)
{
// TODO: show error
}
}
private bool AddProjects(MainViewModel mainViewModel, ProjectsData.ProjectsListWrapper projects)
{
foreach (var project in projects.Projects)
{
mainViewModel.Projects.Add(GetProjectFromWrappper(project));
}
mainViewModel.Initialize();
return true;
}
private bool SetProjects(MainViewModel mainViewModel, ProjectsData.ProjectsListWrapper projects)
{
mainViewModel.Projects = new System.Collections.ObjectModel.ObservableCollection<Project> { };
return AddProjects(mainViewModel, projects);
}
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
namespace ProjectsEditor.Utils
{
public static class StringUtils
{
public static string UpperCamelCaseToDashCase(this string str)
{
// If it's single letter variable, leave it as it is
if (str.Length == 1)
{
return str;
}
return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "-" + x.ToString() : x.ToString())).ToLowerInvariant();
}
}
}

View File

@@ -0,0 +1,511 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using ProjectsEditor.Models;
using ProjectsEditor.Utils;
using Windows.ApplicationModel.Core;
using Windows.Management.Deployment;
using static ProjectsEditor.Data.ProjectsData;
namespace ProjectsEditor.ViewModels
{
public class MainViewModel : INotifyPropertyChanged, IDisposable
{
private ProjectsEditorIO _projectsEditorIO;
public ObservableCollection<Project> Projects { get; set; }
public IEnumerable<Project> ProjectsView
{
get
{
IEnumerable<Project> projects = string.IsNullOrEmpty(_searchTerm) ? Projects : Projects.Where(x => x.Name.Contains(_searchTerm, StringComparison.InvariantCultureIgnoreCase));
if (projects == null)
{
return Enumerable.Empty<Project>();
}
OrderBy orderBy = (OrderBy)_orderByIndex;
if (orderBy == OrderBy.LastViewed)
{
return projects.OrderByDescending(x => x.LastLaunchedTime);
}
else if (orderBy == OrderBy.Created)
{
return projects.OrderByDescending(x => x.CreationTime);
}
else
{
return projects.OrderBy(x => x.Name);
}
}
}
private string _searchTerm;
public string SearchTerm
{
get => _searchTerm;
set
{
_searchTerm = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(ProjectsView)));
}
}
private int _orderByIndex;
public int OrderByIndex
{
get => _orderByIndex;
set
{
_orderByIndex = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(ProjectsView)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChanged?.Invoke(this, e);
}
private Project editedProject;
private bool isEditedProjectNewlyCreated;
private string projectNameBeingEdited;
private MainWindow _mainWindow;
private System.Timers.Timer lastUpdatedTimer;
public MainViewModel(ProjectsEditorIO projectsEditorIO)
{
_projectsEditorIO = projectsEditorIO;
lastUpdatedTimer = new System.Timers.Timer();
lastUpdatedTimer.Interval = 1000;
lastUpdatedTimer.Elapsed += LastUpdatedTimerElapsed;
lastUpdatedTimer.Start();
}
public void Initialize()
{
foreach (Project project in Projects)
{
project.Initialize();
}
OnPropertyChanged(new PropertyChangedEventArgs(nameof(ProjectsView)));
}
public void SetEditedProject(Project editedProject)
{
this.editedProject = editedProject;
}
public void SaveProject(Project projectToSave)
{
editedProject.Name = projectToSave.Name;
editedProject.IsShortcutNeeded = projectToSave.IsShortcutNeeded;
editedProject.PreviewImage = projectToSave.PreviewImage;
for (int appIndex = 0; appIndex < editedProject.Applications.Count; appIndex++)
{
editedProject.Applications[appIndex].IsSelected = projectToSave.Applications[appIndex].IsSelected;
editedProject.Applications[appIndex].CommandLineArguments = projectToSave.Applications[appIndex].CommandLineArguments;
}
editedProject.OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("AppsCountString"));
editedProject.Initialize();
_projectsEditorIO.SerializeProjects(Projects.ToList());
if (editedProject.IsShortcutNeeded)
{
CreateShortcut(editedProject);
}
}
private void CreateShortcut(Project editedProject)
{
object shDesktop = (object)"Desktop";
IWshRuntimeLibrary.WshShell shell = new IWshRuntimeLibrary.WshShell();
string shortcutAddress = (string)shell.SpecialFolders.Item(ref shDesktop) + $"\\{editedProject.Name}.lnk";
IWshRuntimeLibrary.IWshShortcut shortcut = (IWshRuntimeLibrary.IWshShortcut)shell.CreateShortcut(shortcutAddress);
shortcut.Description = $"Project Launcher {editedProject.Id}";
string basePath = AppDomain.CurrentDomain.BaseDirectory;
shortcut.TargetPath = Path.Combine(basePath, "ProjectsEditor.exe");
shortcut.Arguments = '"' + editedProject.Id + '"';
shortcut.WorkingDirectory = basePath;
shortcut.Save();
}
public void SaveProjectName(Project project)
{
projectNameBeingEdited = project.Name;
}
public void CancelProjectName(Project project)
{
project.Name = projectNameBeingEdited;
}
public async void AddNewProject()
{
await Task.Run(() => RunSnapshotTool());
if (_projectsEditorIO.ParseProjects(this).Result == true && Projects.Count != 0)
{
int repeatCounter = 1;
string newName = Projects.Count != 0 ? Projects.Last().Name : "Project 1"; // TODO: localizable project name
while (Projects.Where(x => x.Name.Equals(Projects.Last().Name, StringComparison.Ordinal)).Count() > 1)
{
Projects.Last().Name = $"{newName} ({repeatCounter})";
repeatCounter++;
}
_projectsEditorIO.SerializeProjects(Projects.ToList());
EditProject(Projects.Last(), true);
}
}
public void EditProject(Project selectedProject, bool isNewlyCreated = false)
{
isEditedProjectNewlyCreated = isNewlyCreated;
var editPage = new ProjectEditor(this);
SetEditedProject(selectedProject);
Project projectEdited = new Project(selectedProject) { EditorWindowTitle = isNewlyCreated ? Properties.Resources.CreateProject : Properties.Resources.EditProject };
if (isNewlyCreated)
{
projectEdited.Initialize();
}
editPage.DataContext = projectEdited;
_mainWindow.ShowPage(editPage);
lastUpdatedTimer.Stop();
}
public void CancelLastEdit()
{
if (isEditedProjectNewlyCreated)
{
Projects.Remove(editedProject);
_projectsEditorIO.SerializeProjects(Projects.ToList());
}
}
public void DeleteProject(Project selectedProject)
{
Projects.Remove(selectedProject);
_projectsEditorIO.SerializeProjects(Projects.ToList());
OnPropertyChanged(new PropertyChangedEventArgs(nameof(ProjectsView)));
}
public void SetMainWindow(MainWindow mainWindow)
{
_mainWindow = mainWindow;
}
public void SwitchToMainView()
{
_mainWindow.SwitchToMainView();
SearchTerm = string.Empty;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(SearchTerm)));
lastUpdatedTimer.Start();
}
public void LaunchProject(string projectId)
{
if (!Projects.Where(x => x.Id == projectId).Any())
{
return;
}
LaunchProject(Projects.Where(x => x.Id == projectId).First(), true);
}
public async void LaunchProject(Project project, bool exitAfterLaunch = false)
{
Project actualSetup = await GetActualSetup();
// Launch apps
// TODO: move launching to ProjectsLauncher
List<Application> newlyStartedApps = new List<Application>();
foreach (Application app in project.Applications.Where(x => x.IsSelected))
{
string launchParam = app.AppPath;
if (string.IsNullOrEmpty(launchParam))
{
continue;
}
if (actualSetup.Applications.Exists(x => x.AppPath.Equals(app.AppPath, StringComparison.Ordinal)))
{
// just move the existing window to the desired position
Application existingApp = actualSetup.Applications.Where(x => x.AppPath.Equals(app.AppPath, StringComparison.Ordinal)).First();
NativeMethods.ShowWindow(existingApp.Hwnd, app.Minimized ? NativeMethods.SW_MINIMIZE : NativeMethods.SW_NORMAL);
if (!app.Minimized)
{
MoveWindowWithScaleAdjustment(project, existingApp.Hwnd, app, true);
NativeMethods.SetForegroundWindow(existingApp.Hwnd);
}
continue;
}
if (launchParam.EndsWith("systemsettings.exe", StringComparison.InvariantCultureIgnoreCase))
{
try
{
string args = string.IsNullOrWhiteSpace(app.CommandLineArguments) ? "home" : app.CommandLineArguments;
bool result = await Windows.System.Launcher.LaunchUriAsync(new Uri($"ms-settings:{args}"));
newlyStartedApps.Add(app);
}
catch (Exception)
{
// todo exception handling
}
}
// todo: check the user's packaged apps folder
else if (app.IsPackagedApp)
{
bool started = false;
int retryCountLaunch = 50;
while (!started && retryCountLaunch > 0)
{
try
{
if (string.IsNullOrEmpty(app.CommandLineArguments))
{
// the official app launching method.No parameters are expected
var packApp = await GetAppByPackageFamilyNameAsync(app.PackagedId);
if (packApp != null)
{
await packApp.LaunchAsync();
}
newlyStartedApps.Add(app);
started = true;
}
else
{
await Task.Run(() =>
{
Process p = new Process();
p.StartInfo = new ProcessStartInfo("cmd.exe");
p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
p.StartInfo.Arguments = $"cmd /c start shell:appsfolder\\{app.Aumid} {app.CommandLineArguments}";
p.EnableRaisingEvents = true;
p.ErrorDataReceived += (sender, e) =>
{
started = false;
};
p.Start();
p.WaitForExit();
});
newlyStartedApps.Add(app);
started = true;
}
}
catch (Exception)
{
// todo exception handling
Thread.Sleep(100);
}
retryCountLaunch--;
}
}
else
{
try
{
await Task.Run(() =>
{
Process p = new Process();
p.StartInfo = new ProcessStartInfo(launchParam);
p.StartInfo.Arguments = app.CommandLineArguments;
p.Start();
});
newlyStartedApps.Add(app);
}
catch (Exception)
{
// try again as admin
try
{
await Task.Run(() =>
{
Process p = new Process();
p.StartInfo = new ProcessStartInfo(launchParam);
p.StartInfo.Arguments = app.CommandLineArguments;
p.StartInfo.UseShellExecute = true;
p.StartInfo.Verb = "runas"; // administrator privilages, some apps start only that way
p.Start();
});
newlyStartedApps.Add(app);
}
catch (Exception)
{
// todo exception handling
}
}
}
}
// the official launching method needs this task.
static async Task<AppListEntry> GetAppByPackageFamilyNameAsync(string packageFamilyName)
{
var pkgManager = new PackageManager();
var pkg = pkgManager.FindPackagesForUser(string.Empty, packageFamilyName).FirstOrDefault();
if (pkg == null)
{
return null;
}
var apps = await pkg.GetAppListEntriesAsync();
var firstApp = apps.Any() ? apps[0] : null;
return firstApp;
}
// next step: move newly created apps to their desired position
// the OS needs some time to show the apps, so do it in multiple steps until all windows all in place
// retry every 100ms for 5 sec totally
int retryCount = 50;
while (newlyStartedApps.Count > 0 && retryCount > 0)
{
List<Application> finishedApps = new List<Application>();
actualSetup = await GetActualSetup();
foreach (Application app in newlyStartedApps)
{
IEnumerable<Application> candidates = actualSetup.Applications.Where(x => app.IsMyAppPath(x.AppPath));
if (candidates.Any())
{
if (app.AppPath.EndsWith("PowerToys.Settings.exe", StringComparison.Ordinal))
{
// give it time to not get confused (the app tries to position itself)
Thread.Sleep(1000);
}
else
{
// the other apps worked fine and reacted correctly to the MoveWindow event, but to be safe, give them also a little time
Thread.Sleep(100);
}
// just move the existing window to the desired position
Application existingApp = candidates.First();
NativeMethods.ShowWindow(existingApp.Hwnd, app.Minimized ? NativeMethods.SW_MINIMIZE : NativeMethods.SW_NORMAL);
if (!app.Minimized)
{
MoveWindowWithScaleAdjustment(project, existingApp.Hwnd, app, true);
NativeMethods.SetForegroundWindow(existingApp.Hwnd);
}
finishedApps.Add(app);
}
}
foreach (Application app in finishedApps)
{
newlyStartedApps.Remove(app);
}
Thread.Sleep(100);
retryCount--;
}
// update last launched time
var ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
project.LastLaunchedTime = (long)ts.TotalSeconds;
_projectsEditorIO.SerializeProjects(Projects.ToList());
OnPropertyChanged(new PropertyChangedEventArgs(nameof(ProjectsView)));
/*await Task.Run(() => RunLauncher(project.Id));
if (_projectsEditorIO.ParseProjects(this).Result == true)
{
OnPropertyChanged(new PropertyChangedEventArgs("ProjectsView"));
}*/
if (exitAfterLaunch)
{
Environment.Exit(0);
}
}
private void LastUpdatedTimerElapsed(object sender, ElapsedEventArgs e)
{
if (Projects == null)
{
return;
}
foreach (Project project in Projects)
{
project.OnPropertyChanged(new PropertyChangedEventArgs("LastLaunched"));
}
}
private void MoveWindowWithScaleAdjustment(Project project, nint hwnd, Application app, bool repaint)
{
NativeMethods.SetForegroundWindow(hwnd);
NativeMethods.MoveWindow(hwnd, app.ScaledPosition.X, app.ScaledPosition.Y, app.ScaledPosition.Width, app.ScaledPosition.Height, repaint);
}
private async Task<Project> GetActualSetup()
{
string filename = Path.GetTempFileName();
if (File.Exists(filename))
{
File.Delete(filename);
}
await Task.Run(() => RunSnapshotTool(filename));
Project actualProject;
_projectsEditorIO.ParseProject(filename, out actualProject);
if (File.Exists(filename))
{
File.Delete(filename);
}
return actualProject;
}
private void RunSnapshotTool(string filename = null)
{
Process p = new Process();
p.StartInfo = new ProcessStartInfo(@".\ProjectsSnapshotTool.exe");
p.StartInfo.CreateNoWindow = true;
if (!string.IsNullOrEmpty(filename))
{
p.StartInfo.Arguments = filename;
}
p.Start();
p.WaitForExit();
}
private void RunLauncher(string projectId)
{
Process p = new Process();
p.StartInfo = new ProcessStartInfo(@".\ProjectsLauncher.exe", projectId);
p.StartInfo.CreateNoWindow = true;
p.Start();
p.WaitForExit();
}
public void Dispose()
{
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- UAC Manifest Options
If you want to change the Windows User Account Control level replace the
requestedExecutionLevel node with one of the following.
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
Specifying requestedExecutionLevel element will disable file and registry virtualization.
Remove this element if your application requires this virtualization for backwards
compatibility.
-->
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows Vista -->
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
<!-- Windows 7 -->
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
<!-- Windows 8 -->
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
<!-- Windows 8.1 -->
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher
DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need
to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should
also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config. -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
<!--
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
-->
</assembly>

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1,653 @@
#include "pch.h"
#include "AppLauncher.h"
#include <TlHelp32.h>
#include <future>
#include <iostream>
#include <string>
#include <vector>
#include <ShellScalingApi.h>
#include <winrt/Windows.UI.Notifications.h>
#include <winrt/Windows.Data.Xml.Dom.h>
#include "../projects-common/MonitorEnumerator.h"
#include "../projects-common/WindowEnumerator.h"
namespace FancyZones
{
inline void SizeWindowToRect(HWND window, RECT rect, BOOL snapZone) noexcept;
}
namespace Common
{
namespace Display
{
namespace DPIAware
{
enum AwarenessLevel
{
UNAWARE,
SYSTEM_AWARE,
PER_MONITOR_AWARE,
PER_MONITOR_AWARE_V2,
UNAWARE_GDISCALED
};
AwarenessLevel GetAwarenessLevel(DPI_AWARENESS_CONTEXT system_returned_value)
{
const std::array levels{ DPI_AWARENESS_CONTEXT_UNAWARE,
DPI_AWARENESS_CONTEXT_SYSTEM_AWARE,
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE,
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED };
for (size_t i = 0; i < size(levels); ++i)
{
if (AreDpiAwarenessContextsEqual(levels[i], system_returned_value))
{
return static_cast<DPIAware::AwarenessLevel>(i);
}
}
return AwarenessLevel::UNAWARE;
}
}
}
namespace Utils
{
namespace Elevation
{
// Run command as non-elevated user, returns true if succeeded, puts the process id into returnPid if returnPid != NULL
inline bool run_non_elevated(const std::wstring& file, const std::wstring& params, DWORD* returnPid, const wchar_t* workingDir = nullptr, const bool showWindow = true, const RECT& windowRect = {})
{
//Logger::info(L"run_non_elevated with params={}", params);
auto executable_args = L"\"" + file + L"\"";
if (!params.empty())
{
executable_args += L" " + params;
}
HWND hwnd = GetShellWindow();
if (!hwnd)
{
if (GetLastError() == ERROR_SUCCESS)
{
//Logger::warn(L"GetShellWindow() returned null. Shell window is not available");
}
else
{
//Logger::error(L"GetShellWindow() failed. {}", get_last_error_or_default(GetLastError()));
}
return false;
}
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
winrt::handle process{ OpenProcess(PROCESS_CREATE_PROCESS, FALSE, pid) };
if (!process)
{
//Logger::error(L"OpenProcess() failed. {}", get_last_error_or_default(GetLastError()));
return false;
}
SIZE_T size = 0;
InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
auto pproc_buffer = std::make_unique<char[]>(size);
auto pptal = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(pproc_buffer.get());
if (!pptal)
{
//Logger::error(L"pptal failed to initialize. {}", get_last_error_or_default(GetLastError()));
return false;
}
if (!InitializeProcThreadAttributeList(pptal, 1, 0, &size))
{
//Logger::error(L"InitializeProcThreadAttributeList() failed. {}", get_last_error_or_default(GetLastError()));
return false;
}
HANDLE process_handle = process.get();
if (!UpdateProcThreadAttribute(pptal,
0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
&process_handle,
sizeof(process_handle),
nullptr,
nullptr))
{
//Logger::error(L"UpdateProcThreadAttribute() failed. {}", get_last_error_or_default(GetLastError()));
return false;
}
STARTUPINFOEX siex = { 0 };
siex.lpAttributeList = pptal;
siex.StartupInfo.cb = sizeof(siex);
PROCESS_INFORMATION pi = { 0 };
auto dwCreationFlags = EXTENDED_STARTUPINFO_PRESENT;
if (!showWindow)
{
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
siex.StartupInfo.wShowWindow = SW_HIDE;
dwCreationFlags = CREATE_NO_WINDOW;
}
else
{
siex.StartupInfo.dwFlags = STARTF_USEPOSITION | STARTF_USESIZE;
siex.StartupInfo.dwX = windowRect.left;
siex.StartupInfo.dwY = windowRect.top;
siex.StartupInfo.dwXSize = windowRect.right - windowRect.left;
siex.StartupInfo.dwYSize = windowRect.bottom - windowRect.top;
}
auto succeeded = CreateProcessW(file.c_str(),
&executable_args[0],
nullptr,
nullptr,
FALSE,
dwCreationFlags,
nullptr,
workingDir,
&siex.StartupInfo,
&pi);
if (succeeded)
{
if (pi.hProcess)
{
if (returnPid)
{
*returnPid = GetProcessId(pi.hProcess);
}
CloseHandle(pi.hProcess);
}
if (pi.hThread)
{
CloseHandle(pi.hThread);
}
}
else
{
//Logger::error(L"CreateProcessW() failed. {}", get_last_error_or_default(GetLastError()));
}
return succeeded;
}
}
}
}
namespace FancyZones
{
inline bool allMonitorsHaveSameDpiScaling()
{
auto monitors = MonitorEnumerator::Enumerate();
if (monitors.size() < 2)
{
return true;
}
UINT firstMonitorDpiX;
UINT firstMonitorDpiY;
if (S_OK != GetDpiForMonitor(monitors[0].first, MDT_EFFECTIVE_DPI, &firstMonitorDpiX, &firstMonitorDpiY))
{
return false;
}
for (int i = 1; i < monitors.size(); i++)
{
UINT iteratedMonitorDpiX;
UINT iteratedMonitorDpiY;
if (S_OK != GetDpiForMonitor(monitors[i].first, MDT_EFFECTIVE_DPI, &iteratedMonitorDpiX, &iteratedMonitorDpiY) ||
iteratedMonitorDpiX != firstMonitorDpiX)
{
return false;
}
}
return true;
}
inline void ScreenToWorkAreaCoords(HWND window, RECT& rect)
{
// First, find the correct monitor. The monitor cannot be found using the given rect itself, we must first
// translate it to relative workspace coordinates.
HMONITOR monitor = MonitorFromRect(&rect, MONITOR_DEFAULTTOPRIMARY);
MONITORINFOEXW monitorInfo{ sizeof(MONITORINFOEXW) };
GetMonitorInfoW(monitor, &monitorInfo);
auto xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left;
auto yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top;
auto referenceRect = rect;
referenceRect.left -= xOffset;
referenceRect.right -= xOffset;
referenceRect.top -= yOffset;
referenceRect.bottom -= yOffset;
// Now, this rect should be used to determine the monitor and thus taskbar size. This fixes
// scenarios where the zone lies approximately between two monitors, and the taskbar is on the left.
monitor = MonitorFromRect(&referenceRect, MONITOR_DEFAULTTOPRIMARY);
GetMonitorInfoW(monitor, &monitorInfo);
xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left;
yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top;
rect.left -= xOffset;
rect.right -= xOffset;
rect.top -= yOffset;
rect.bottom -= yOffset;
const auto level = Common::Display::DPIAware::GetAwarenessLevel(GetWindowDpiAwarenessContext(window));
const bool accountForUnawareness = level < Common::Display::DPIAware::PER_MONITOR_AWARE;
if (accountForUnawareness && !allMonitorsHaveSameDpiScaling())
{
rect.left = max(monitorInfo.rcMonitor.left, rect.left);
rect.right = min(monitorInfo.rcMonitor.right - xOffset, rect.right);
rect.top = max(monitorInfo.rcMonitor.top, rect.top);
rect.bottom = min(monitorInfo.rcMonitor.bottom - yOffset, rect.bottom);
}
}
inline void SizeWindowToRect(HWND window, RECT rect, BOOL snapZone) noexcept
{
WINDOWPLACEMENT placement{};
::GetWindowPlacement(window, &placement);
// Wait if SW_SHOWMINIMIZED would be removed from window (Issue #1685)
for (int i = 0; i < 5 && (placement.showCmd == SW_SHOWMINIMIZED); ++i)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
::GetWindowPlacement(window, &placement);
}
BOOL maximizeLater = false;
if (IsWindowVisible(window))
{
// If is not snap zone then need keep maximize state (move to active monitor)
if (!snapZone && placement.showCmd == SW_SHOWMAXIMIZED)
{
maximizeLater = true;
}
// Do not restore minimized windows. We change their placement though so they restore to the correct zone.
if ((placement.showCmd != SW_SHOWMINIMIZED) &&
(placement.showCmd != SW_MINIMIZE))
{
// Remove maximized show command to make sure window is moved to the correct zone.
if (placement.showCmd == SW_SHOWMAXIMIZED)
placement.flags &= ~WPF_RESTORETOMAXIMIZED;
placement.showCmd = SW_RESTORE;
}
}
else
{
placement.showCmd = SW_HIDE;
}
ScreenToWorkAreaCoords(window, rect);
placement.rcNormalPosition = rect;
placement.flags |= WPF_ASYNCWINDOWPLACEMENT;
std::wcout << "Set window placement" << std::endl;
auto result = ::SetWindowPlacement(window, &placement);
if (!result)
{
std::wcout << "Set window placement failed" << std::endl;
//Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError()));
}
// make sure window is moved to the correct monitor before maximize.
if (maximizeLater)
{
placement.showCmd = SW_SHOWMAXIMIZED;
}
// Do it again, allowing Windows to resize the window and set correct scaling
// This fixes Issue #365
result = ::SetWindowPlacement(window, &placement);
if (!result)
{
std::wcout << "Set window placement failed" << std::endl;
//Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError()));
}
}
}
namespace KBM
{
using namespace winrt;
using namespace Windows::UI::Notifications;
using namespace Windows::Data::Xml::Dom;
// Use to find a process by its name
std::wstring GetFileNameFromPath(const std::wstring& fullPath)
{
size_t found = fullPath.find_last_of(L"\\");
if (found != std::wstring::npos)
{
return fullPath.substr(found + 1);
}
return fullPath;
}
DWORD GetProcessIdByName(const std::wstring& processName)
{
DWORD pid = 0;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(snapshot, &processEntry))
{
do
{
if (_wcsicmp(processEntry.szExeFile, processName.c_str()) == 0)
{
pid = processEntry.th32ProcessID;
break;
}
} while (Process32Next(snapshot, &processEntry));
}
CloseHandle(snapshot);
}
return pid;
}
std::vector<DWORD> GetProcessesIdByName(const std::wstring& processName)
{
std::vector<DWORD> processIds;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(snapshot, &processEntry))
{
do
{
if (_wcsicmp(processEntry.szExeFile, processName.c_str()) == 0)
{
processIds.push_back(processEntry.th32ProcessID);
}
} while (Process32Next(snapshot, &processEntry));
}
CloseHandle(snapshot);
}
return processIds;
}
struct handle_data
{
unsigned long process_id;
HWND window_handle;
};
// used by FindMainWindow
BOOL CALLBACK EnumWindowsCallbackAllowNonVisible(HWND handle, LPARAM lParam)
{
handle_data& data = *reinterpret_cast<handle_data*>(lParam);
unsigned long process_id = 0;
GetWindowThreadProcessId(handle, &process_id);
if (data.process_id == process_id)
{
data.window_handle = handle;
return FALSE;
}
return TRUE;
}
// used by FindMainWindow
BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam)
{
handle_data& data = *reinterpret_cast<handle_data*>(lParam);
unsigned long process_id = 0;
GetWindowThreadProcessId(handle, &process_id);
if (data.process_id != process_id || !(GetWindow(handle, GW_OWNER) == static_cast<HWND>(0) && IsWindowVisible(handle)))
{
return TRUE;
}
data.window_handle = handle;
return FALSE;
}
// used for reactivating a window for a program we already started.
HWND FindMainWindow(unsigned long process_id, const bool allowNonVisible)
{
handle_data data;
data.process_id = process_id;
data.window_handle = 0;
if (allowNonVisible)
{
EnumWindows(EnumWindowsCallbackAllowNonVisible, reinterpret_cast<LPARAM>(&data));
}
else
{
EnumWindows(EnumWindowsCallback, reinterpret_cast<LPARAM>(&data));
}
return data.window_handle;
}
bool ShowProgram(DWORD pid, std::wstring programName, bool isNewProcess, bool minimizeIfVisible, const RECT& windowPosition, int retryCount)
{
// a good place to look for this...
// https://github.com/ritchielawrence/cmdow
// try by main window.
auto allowNonVisible = true;
HWND hwnd = FindMainWindow(pid, allowNonVisible);
if (hwnd == NULL)
{
if (retryCount < 20)
{
auto future = std::async(std::launch::async, [=] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
ShowProgram(pid, programName, isNewProcess, minimizeIfVisible, windowPosition, retryCount + 1);
return false;
});
}
}
else
{
if (hwnd == GetForegroundWindow())
{
// only hide if this was a call from a already open program, don't make small if we just opened it.
if (!isNewProcess && minimizeIfVisible)
{
return ShowWindow(hwnd, SW_MINIMIZE);
}
FancyZones::SizeWindowToRect(hwnd, windowPosition, false);
return false;
}
else
{
// Check if the window is minimized
if (IsIconic(hwnd))
{
// Show the window since SetForegroundWindow fails on minimized windows
if (!ShowWindow(hwnd, SW_RESTORE))
{
std::wcout << L"ShowWindow failed" << std::endl;
}
}
//INPUT inputs[1] = { {.type = INPUT_MOUSE } };
//SendInput(ARRAYSIZE(inputs), inputs, sizeof(INPUT));
if (!SetForegroundWindow(hwnd))
{
return false;
}
else
{
FancyZones::SizeWindowToRect(hwnd, windowPosition, false);
return true;
}
}
}
if (isNewProcess)
{
return true;
}
if (false)
{
// try by console.
hwnd = FindWindow(nullptr, nullptr);
if (AttachConsole(pid))
{
// Get the console window handle
hwnd = GetConsoleWindow();
auto showByConsoleSuccess = false;
if (hwnd != NULL)
{
ShowWindow(hwnd, SW_RESTORE);
if (SetForegroundWindow(hwnd))
{
showByConsoleSuccess = true;
}
}
// Detach from the console
FreeConsole();
if (showByConsoleSuccess)
{
return true;
}
}
}
// try to just show them all (if they have a title)!.
hwnd = FindWindow(nullptr, nullptr);
if (hwnd)
{
while (hwnd)
{
DWORD pidForHwnd;
GetWindowThreadProcessId(hwnd, &pidForHwnd);
if (pid == pidForHwnd)
{
int length = GetWindowTextLength(hwnd);
if (length > 0)
{
ShowWindow(hwnd, SW_RESTORE);
// hwnd is the window handle with targetPid
if (SetForegroundWindow(hwnd))
{
FancyZones::SizeWindowToRect(hwnd, windowPosition, false);
return true;
}
}
}
hwnd = FindWindowEx(NULL, hwnd, NULL, NULL);
}
}
return false;
}
bool HideProgram(DWORD pid, std::wstring programName, int retryCount)
{
HWND hwnd = FindMainWindow(pid, false);
if (hwnd == NULL)
{
if (retryCount < 20)
{
auto future = std::async(std::launch::async, [=] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
HideProgram(pid, programName, retryCount + 1);
return false;
});
}
}
hwnd = FindWindow(nullptr, nullptr);
while (hwnd)
{
DWORD pidForHwnd;
GetWindowThreadProcessId(hwnd, &pidForHwnd);
if (pid == pidForHwnd)
{
if (IsWindowVisible(hwnd))
{
ShowWindow(hwnd, SW_HIDE);
}
}
hwnd = FindWindowEx(NULL, hwnd, NULL, NULL);
}
return true;
}
}
void Launch(const std::wstring& appPath, bool startMinimized, const std::wstring& commandLineArgs, const RECT& windowPosition) noexcept
{
WCHAR fullExpandedFilePath[MAX_PATH];
ExpandEnvironmentStrings(appPath.c_str(), fullExpandedFilePath, MAX_PATH);
auto fileNamePart = KBM::GetFileNameFromPath(fullExpandedFilePath);
DWORD dwAttrib = GetFileAttributesW(fullExpandedFilePath);
if (dwAttrib == INVALID_FILE_ATTRIBUTES)
{
return;
}
DWORD processId = 0;
if (Common::Utils::Elevation::run_non_elevated(fullExpandedFilePath, commandLineArgs, &processId, nullptr, !startMinimized))
{
std::wcout << "Launched " << fileNamePart << std::endl;
}
else
{
std::wcout << "Failed to launch " << fileNamePart << std::endl;
}
if (processId == 0)
{
return;
}
auto targetPid = KBM::GetProcessIdByName(fileNamePart);
if (!startMinimized)
{
KBM::ShowProgram(targetPid, fileNamePart, false, false, windowPosition, 0);
}
else
{
KBM::HideProgram(targetPid, fileNamePart, 0);
}
return;
}

View File

@@ -0,0 +1,5 @@
#pragma once
#include <Windows.h>
void Launch(const std::wstring& appPath, bool startMinimized, const std::wstring& commandLineArgs, const RECT& rect) noexcept;

View File

@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<CppWinRTOptimized>true</CppWinRTOptimized>
<CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
<CppWinRTGenerateWindowsMetadata>true</CppWinRTGenerateWindowsMetadata>
<MinimalCoreWin>true</MinimalCoreWin>
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{2cac093e-5fcf-4102-9c2c-ac7dd5d9eb96}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>ProjectsLauncher</RootNamespace>
<WindowsTargetPlatformVersion Condition=" '$(WindowsTargetPlatformVersion)' == '' ">10.0.22621.0</WindowsTargetPlatformVersion>
<WindowsTargetPlatformMinVersion>10.0.17134.0</WindowsTargetPlatformMinVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '16.0'">v142</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '15.0'">v141</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '14.0'">v140</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="PropertySheet.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>
<PreprocessorDefinitions>_CONSOLE;WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<WarningLevel>Level4</WarningLevel>
<AdditionalOptions>%(AdditionalOptions) /permissive- /bigobj</AdditionalOptions>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<LanguageStandard Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">shcore.lib;Shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Platform)'=='Win32'">
<ClCompile>
<PreprocessorDefinitions>WIN32;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<LanguageStandard Condition="'$(Configuration)|$(Platform)'=='Release|x64'">stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">shcore.lib;Shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="AppLauncher.h" />
<ClInclude Include="pch.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="AppLauncher.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="PropertySheet.props" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="AppLauncher.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="AppLauncher.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="PropertySheet.props" />
<None Include="packages.config" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="PropertySheets" />
<PropertyGroup Label="UserMacros" />
<!--
To customize common C++/WinRT project properties:
* right-click the project node
* expand the Common Properties item
* select the C++/WinRT property page
For more advanced scenarios, and complete documentation, please see:
https://github.com/Microsoft/cppwinrt/tree/master/nuget
-->
<PropertyGroup />
<ItemDefinitionGroup />
</Project>

View File

@@ -0,0 +1,71 @@
#include "pch.h"
#include <iostream>
#include "../projects-common/Data.h"
#include "AppLauncher.h"
int main(int argc, char* argv[])
{
// read projects
std::vector<Project> projects;
try
{
auto savedProjectsJson = json::from_file(JsonUtils::ProjectsFile());
if (savedProjectsJson.has_value())
{
auto savedProjects = JsonUtils::ProjectsListJSON::FromJson(savedProjectsJson.value());
if (savedProjects.has_value())
{
projects = savedProjects.value();
}
}
}
catch (std::exception)
{
return 1;
}
if (projects.empty())
{
return 1;
}
Project projectToLaunch = projects[0];
if (argc > 1)
{
std::string idStr = argv[1];
std::wstring id(idStr.begin(), idStr.end());
for (const auto& proj : projects)
{
if (proj.id == id)
{
projectToLaunch = proj;
break;
}
}
}
// launch apps
for (const auto& app : projectToLaunch.apps)
{
Launch(app.appPath, app.isMinimized, app.commandLineArgs, app.position.toRect());
}
// update last-launched time
time_t launchedTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
projectToLaunch.lastLaunchedTime = launchedTime;
for (int i = 0; i < projects.size(); i++)
{
if (projects[i].id == projectToLaunch.id)
{
projects[i] = projectToLaunch;
break;
}
}
json::to_file(JsonUtils::ProjectsFile(), JsonUtils::ProjectsListJSON::ToJson(projects));
return 0;
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.220531.1" targetFramework="native" />
</packages>

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,3 @@
#pragma once
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>

View File

@@ -0,0 +1,215 @@
#include "pch.h"
#include "MonitorUtils.h"
#include <ShellScalingApi.h>
#include "../projects-common/MonitorEnumerator.h"
#include "OnThreadExecutor.h"
namespace Common
{
namespace Display
{
namespace DPIAware
{
constexpr inline int DEFAULT_DPI = 96;
void Convert(HMONITOR monitor_handle, float& width, float& height)
{
if (monitor_handle == NULL)
{
const POINT ptZero = { 0, 0 };
monitor_handle = MonitorFromPoint(ptZero, MONITOR_DEFAULTTOPRIMARY);
}
UINT dpi_x, dpi_y;
if (GetDpiForMonitor(monitor_handle, MDT_EFFECTIVE_DPI, &dpi_x, &dpi_y) == S_OK)
{
width = width * dpi_x / DEFAULT_DPI;
height = height * dpi_y / DEFAULT_DPI;
}
}
HRESULT GetScreenDPIForMonitor(HMONITOR targetMonitor, UINT& dpi)
{
if (targetMonitor != nullptr)
{
UINT dummy = 0;
return GetDpiForMonitor(targetMonitor, MDT_EFFECTIVE_DPI, &dpi, &dummy);
}
else
{
dpi = DPIAware::DEFAULT_DPI;
return E_FAIL;
}
}
}
}
}
namespace MonitorUtils
{
namespace Display
{
constexpr inline bool not_digit(wchar_t ch)
{
return '0' <= ch && ch <= '9';
}
std::wstring remove_non_digits(const std::wstring& input)
{
std::wstring result;
std::copy_if(input.begin(), input.end(), std::back_inserter(result), not_digit);
return result;
}
std::pair<bool, std::vector<Project::Monitor>> GetDisplays()
{
bool success = true;
std::vector<Project::Monitor> result{};
auto allMonitors = MonitorEnumerator::Enumerate();
for (auto& monitorData : allMonitors)
{
MONITORINFOEX monitorInfo = monitorData.second;
MONITORINFOEX dpiUnawareMonitorInfo{};
OnThreadExecutor dpiUnawareThread;
dpiUnawareThread.submit(OnThreadExecutor::task_t{ [&] {
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
SetThreadDpiHostingBehavior(DPI_HOSTING_BEHAVIOR_MIXED);
dpiUnawareMonitorInfo.cbSize = sizeof(dpiUnawareMonitorInfo);
if (!GetMonitorInfo(monitorData.first, &dpiUnawareMonitorInfo))
{
return;
}
} }).wait();
float width = static_cast<float>(monitorInfo.rcMonitor.right - monitorInfo.rcMonitor.left);
float height = static_cast<float>(monitorInfo.rcMonitor.bottom - monitorInfo.rcMonitor.top);
float dpiUnawareWidth = static_cast<float>(dpiUnawareMonitorInfo.rcMonitor.right - dpiUnawareMonitorInfo.rcMonitor.left);
float dpiUnawareHeight = static_cast<float>(dpiUnawareMonitorInfo.rcMonitor.bottom - dpiUnawareMonitorInfo.rcMonitor.top);
UINT dpi = 0;
if (Common::Display::DPIAware::GetScreenDPIForMonitor(monitorData.first, dpi) != S_OK)
{
continue;
}
Project::Monitor monitorId{
.monitor = monitorData.first,
.dpi = dpi,
.monitorRectDpiAware = Project::Monitor::MonitorRect {
.top = monitorInfo.rcMonitor.top,
.left = monitorInfo.rcMonitor.left,
.width = static_cast<int>(std::roundf(width)),
.height = static_cast<int>(std::roundf(height)),
},
.monitorRectDpiUnaware = Project::Monitor::MonitorRect {
.top = dpiUnawareMonitorInfo.rcMonitor.top,
.left = dpiUnawareMonitorInfo.rcMonitor.left,
.width = static_cast<int>(std::roundf(dpiUnawareWidth)),
.height = static_cast<int>(std::roundf(dpiUnawareHeight)),
},
};
bool foundActiveMonitor = false;
DISPLAY_DEVICE displayDevice{ .cb = sizeof(displayDevice) };
DWORD displayDeviceIndex = 0;
while (EnumDisplayDevicesW(monitorInfo.szDevice, displayDeviceIndex, &displayDevice, EDD_GET_DEVICE_INTERFACE_NAME))
{
/*
* if (WI_IsFlagSet(displayDevice.StateFlags, DISPLAY_DEVICE_ACTIVE) &&
WI_IsFlagClear(displayDevice.StateFlags, DISPLAY_DEVICE_MIRRORING_DRIVER))
*/
if (((displayDevice.StateFlags & DISPLAY_DEVICE_ACTIVE) == DISPLAY_DEVICE_ACTIVE) &&
(displayDevice.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER) == 0)
{
// Find display devices associated with the display.
foundActiveMonitor = true;
break;
}
displayDeviceIndex++;
}
if (foundActiveMonitor)
{
auto deviceId = SplitDisplayDeviceId(displayDevice.DeviceID);
monitorId.id = deviceId.first;
monitorId.instanceId = deviceId.second;
try
{
std::wstring numberStr = displayDevice.DeviceName; // \\.\DISPLAY1\Monitor0
numberStr = numberStr.substr(0, numberStr.find_last_of('\\')); // \\.\DISPLAY1
numberStr = remove_non_digits(numberStr);
monitorId.number = std::stoi(numberStr);
}
catch (...)
{
monitorId.number = 0;
}
}
else
{
success = false;
// Use the display name as a fallback value when no proper device was found.
monitorId.id = monitorInfo.szDevice;
monitorId.instanceId = L"";
try
{
std::wstring numberStr = monitorInfo.szDevice; // \\.\DISPLAY1
numberStr = remove_non_digits(numberStr);
monitorId.number = std::stoi(numberStr);
}
catch (...)
{
monitorId.number = 0;
}
}
result.push_back(std::move(monitorId));
}
return { success, result };
}
std::pair<std::wstring, std::wstring> SplitDisplayDeviceId(const std::wstring& str) noexcept
{
// format: \\?\DISPLAY#{device id}#{instance id}#{some other id}
// example: \\?\DISPLAY#GSM1388#4&125707d6&0&UID8388688#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}
// output: { GSM1388, 4&125707d6&0&UID8388688 }
size_t nameStartPos = str.find_first_of('#');
size_t uidStartPos = str.find('#', nameStartPos + 1);
size_t uidEndPos = str.find('#', uidStartPos + 1);
if (nameStartPos == std::string::npos || uidStartPos == std::string::npos || uidEndPos == std::string::npos)
{
return { str, L"" };
}
return { str.substr(nameStartPos + 1, uidStartPos - nameStartPos - 1), str.substr(uidStartPos + 1, uidEndPos - uidStartPos - 1) };
}
}
std::vector<Project::Monitor> IdentifyMonitors() noexcept
{
auto displaysResult = Display::GetDisplays();
// retry
int retryCounter = 0;
while (!displaysResult.first && retryCounter < 100)
{
std::this_thread::sleep_for(std::chrono::milliseconds(30));
displaysResult = Display::GetDisplays();
retryCounter++;
}
return displaysResult.second;
}
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include <vector>
#include "../projects-common/Data.h"
// FancyZones: MonitorUtils.h
namespace MonitorUtils
{
namespace Display
{
std::pair<bool, std::vector<Project::Monitor>> GetDisplays();
std::pair<std::wstring, std::wstring> SplitDisplayDeviceId(const std::wstring& str) noexcept;
}
std::vector<Project::Monitor> IdentifyMonitors() noexcept;
}

View File

@@ -0,0 +1,51 @@
#include "pch.h"
#include "OnThreadExecutor.h"
OnThreadExecutor::OnThreadExecutor() :
_shutdown_request{ false }, _worker_thread{ [this] { worker_thread(); } }
{
}
std::future<void> OnThreadExecutor::submit(task_t task)
{
auto future = task.get_future();
std::lock_guard lock{ _task_mutex };
_task_queue.emplace(std::move(task));
_task_cv.notify_one();
return future;
}
void OnThreadExecutor::cancel()
{
std::lock_guard lock{ _task_mutex };
_task_queue = {};
_task_cv.notify_one();
}
void OnThreadExecutor::worker_thread()
{
while (!_shutdown_request)
{
task_t task;
{
std::unique_lock task_lock{ _task_mutex };
_task_cv.wait(task_lock, [this] { return !_task_queue.empty() || _shutdown_request; });
if (_shutdown_request)
{
return;
}
task = std::move(_task_queue.front());
_task_queue.pop();
}
task();
}
}
OnThreadExecutor::~OnThreadExecutor()
{
_shutdown_request = true;
_task_cv.notify_one();
_worker_thread.join();
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <future>
#include <thread>
#include <functional>
#include <queue>
#include <atomic>
// OnThreadExecutor allows its caller to off-load some work to a persistently running background thread.
// This might come in handy if you use the API which sets thread-wide global state and the state needs
// to be isolated.
class OnThreadExecutor final
{
public:
using task_t = std::packaged_task<void()>;
OnThreadExecutor();
~OnThreadExecutor();
std::future<void> submit(task_t task);
void cancel();
private:
void worker_thread();
std::mutex _task_mutex;
std::condition_variable _task_cv;
std::atomic_bool _shutdown_request;
std::queue<std::packaged_task<void()>> _task_queue;
std::thread _worker_thread;
};

View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<CppWinRTOptimized>true</CppWinRTOptimized>
<CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
<CppWinRTGenerateWindowsMetadata>true</CppWinRTGenerateWindowsMetadata>
<MinimalCoreWin>true</MinimalCoreWin>
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{3d63307b-9d27-44fd-b033-b26f39245b85}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>ProjectsSnapshotTool</RootNamespace>
<WindowsTargetPlatformVersion Condition=" '$(WindowsTargetPlatformVersion)' == '' ">10.0.22621.0</WindowsTargetPlatformVersion>
<WindowsTargetPlatformMinVersion>10.0.17134.0</WindowsTargetPlatformMinVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '16.0'">v142</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '15.0'">v141</PlatformToolset>
<PlatformToolset Condition="'$(VisualStudioVersion)' == '14.0'">v140</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="PropertySheet.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>
<PreprocessorDefinitions>_CONSOLE;WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<WarningLevel>Level4</WarningLevel>
<AdditionalOptions>%(AdditionalOptions) /permissive- /bigobj</AdditionalOptions>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">shcore.lib;Shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Platform)'=='Win32'">
<ClCompile>
<PreprocessorDefinitions>WIN32;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
<AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">shcore.lib;Shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="MonitorUtils.h" />
<ClInclude Include="OnThreadExecutor.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="VirtualDesktop.h" />
<ClInclude Include="WindowFilter.h" />
<ClInclude Include="WindowUtils.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp" />
<ClCompile Include="MonitorUtils.cpp" />
<ClCompile Include="OnThreadExecutor.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="VirtualDesktop.cpp" />
<ClCompile Include="WindowUtils.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="PropertySheet.props" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.220531.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="VirtualDesktop.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WindowFilter.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WindowUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="MonitorUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="OnThreadExecutor.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="VirtualDesktop.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WindowUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="MonitorUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="OnThreadExecutor.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="PropertySheet.props" />
<None Include="packages.config" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="PropertySheets" />
<PropertyGroup Label="UserMacros" />
<!--
To customize common C++/WinRT project properties:
* right-click the project node
* expand the Common Properties item
* select the C++/WinRT property page
For more advanced scenarios, and complete documentation, please see:
https://github.com/Microsoft/cppwinrt/tree/master/nuget
-->
<PropertyGroup />
<ItemDefinitionGroup />
</Project>

View File

@@ -0,0 +1,39 @@
#include "pch.h"
#include "VirtualDesktop.h"
#include <iostream>
VirtualDesktop::VirtualDesktop()
{
auto res = CoCreateInstance(CLSID_VirtualDesktopManager, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&m_vdManager));
if (FAILED(res))
{
// Logger::error("Failed to create VirtualDesktopManager instance");
std::cout << "Failed to create VirtualDesktopManager instance\n";
}
}
VirtualDesktop::~VirtualDesktop()
{
if (m_vdManager)
{
m_vdManager->Release();
}
}
VirtualDesktop& VirtualDesktop::instance()
{
static VirtualDesktop self;
return self;
}
bool VirtualDesktop::IsWindowOnCurrentDesktop(HWND window) const
{
BOOL isWindowOnCurrentDesktop = false;
if (m_vdManager)
{
m_vdManager->IsWindowOnCurrentVirtualDesktop(window, &isWindowOnCurrentDesktop);
}
return isWindowOnCurrentDesktop;
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include <windows.h>
#include <ShObjIdl.h>
class VirtualDesktop
{
public:
static VirtualDesktop& instance();
bool IsWindowOnCurrentDesktop(HWND window) const;
private:
VirtualDesktop();
~VirtualDesktop();
IVirtualDesktopManager* m_vdManager{ nullptr };
};

View File

@@ -0,0 +1,54 @@
#pragma once
#include "VirtualDesktop.h"
#include "WindowUtils.h"
namespace WindowFilter
{
inline bool Filter(HWND window)
{
auto style = GetWindowLong(window, GWL_STYLE);
auto exStyle = GetWindowLong(window, GWL_EXSTYLE);
if (!WindowUtils::HasStyle(style, WS_VISIBLE))
{
return false;
}
if (!IsWindowVisible(window))
{
return false;
}
if (WindowUtils::HasStyle(exStyle, WS_EX_TOOLWINDOW))
{
return false;
}
if (!WindowUtils::IsRoot(window))
{
// child windows such as buttons, combo boxes, etc.
return false;
}
bool isPopup = WindowUtils::HasStyle(style, WS_POPUP);
bool hasThickFrame = WindowUtils::HasStyle(style, WS_THICKFRAME);
bool hasCaption = WindowUtils::HasStyle(style, WS_CAPTION);
bool hasMinimizeMaximizeButtons = WindowUtils::HasStyle(style, WS_MINIMIZEBOX) || WindowUtils::HasStyle(style, WS_MAXIMIZEBOX);
if (isPopup && !(hasThickFrame && (hasCaption || hasMinimizeMaximizeButtons)))
{
// popup windows we want to snap: e.g. Calculator, Telegram
// popup windows we don't want to snap: start menu, notification popup, tray window, etc.
// WS_CAPTION, WS_MINIMIZEBOX, WS_MAXIMIZEBOX are used for filtering out menus,
// e.g., in Edge "Running as admin" menu when creating a new PowerToys issue.
return false;
}
if (!VirtualDesktop::instance().IsWindowOnCurrentDesktop(window))
{
return false;
}
return true;
}
}

View File

@@ -0,0 +1,114 @@
#include "pch.h"
#include "WindowUtils.h"
#include <ShellScalingApi.h>
namespace Common
{
namespace Display
{
namespace DPIAware
{
constexpr inline int DEFAULT_DPI = 96;
void InverseConvert(HMONITOR monitor_handle, float& width, float& height)
{
if (monitor_handle == NULL)
{
const POINT ptZero = { 0, 0 };
monitor_handle = MonitorFromPoint(ptZero, MONITOR_DEFAULTTOPRIMARY);
}
UINT dpi_x, dpi_y;
if (GetDpiForMonitor(monitor_handle, MDT_EFFECTIVE_DPI, &dpi_x, &dpi_y) == S_OK)
{
width = width * DPIAware::DEFAULT_DPI / dpi_x;
height = height * DPIAware::DEFAULT_DPI / dpi_y;
}
}
}
}
}
namespace WindowUtils
{
// Non-Localizable strings
namespace NonLocalizable
{
const wchar_t SystemAppsFolder[] = L"SYSTEMAPPS";
const wchar_t System[] = L"WINDOWS/SYSTEM";
const wchar_t System32[] = L"SYSTEM32";
const wchar_t SystemWOW64[] = L"SYSTEMWOW64";
const char SplashClassName[] = "MsoSplash";
const wchar_t CoreWindow[] = L"WINDOWS.UI.CORE.COREWINDOW";
const wchar_t SearchUI[] = L"SEARCHUI.EXE";
const wchar_t ProjectsSnapshotTool[] = L"PROJECTSSNAPSHOTTOOL";
const wchar_t ProjectsEditor[] = L"PROJECTSEDITOR";
const wchar_t ProjectsLauncher[] = L"PROJECTSLAUNCHER";
}
bool IsRoot(HWND window) noexcept
{
return GetAncestor(window, GA_ROOT) == window;
}
bool IsMaximized(HWND window) noexcept
{
WINDOWPLACEMENT placement{};
if (GetWindowPlacement(window, &placement) &&
placement.showCmd == SW_SHOWMAXIMIZED)
{
return true;
}
return false;
}
bool IsExcludedByDefault(HWND window, const std::wstring& processPath, const std::wstring& title)
{
std::wstring processPathUpper = processPath;
CharUpperBuffW(processPathUpper.data(), static_cast<DWORD>(processPathUpper.length()));
static std::vector<std::wstring> defaultExcludedFolders = { NonLocalizable::SystemAppsFolder, NonLocalizable::System, NonLocalizable::System32, NonLocalizable::SystemWOW64 };
if (Common::Utils::ExcludedApps::find_folder_in_path(processPathUpper, defaultExcludedFolders))
{
return true;
}
std::array<char, 256> className;
GetClassNameA(window, className.data(), static_cast<int>(className.size()));
if (Common::Utils::Window::is_system_window(window, className.data()))
{
return true;
}
if (strcmp(NonLocalizable::SplashClassName, className.data()) == 0)
{
return true;
}
static std::vector<std::wstring> defaultExcludedApps = { NonLocalizable::CoreWindow, NonLocalizable::SearchUI, NonLocalizable::ProjectsEditor, NonLocalizable::ProjectsLauncher, NonLocalizable::ProjectsSnapshotTool };
return (Common::Utils::ExcludedApps::check_excluded_app(processPathUpper, title, defaultExcludedApps));
}
RECT GetWindowRect(HWND window)
{
RECT rect;
if (GetWindowRect(window, &rect))
{
float width = static_cast<float>(rect.right - rect.left);
float height = static_cast<float>(rect.bottom - rect.top);
float originX = static_cast<float>(rect.left);
float originY = static_cast<float>(rect.top);
Common::Display::DPIAware::InverseConvert(MonitorFromWindow(window, MONITOR_DEFAULTTONULL), width, height);
Common::Display::DPIAware::InverseConvert(MonitorFromWindow(window, MONITOR_DEFAULTTONULL), originX, originY);
return RECT(static_cast<LONG>(std::roundf(originX)),
static_cast<LONG>(std::roundf(originY)),
static_cast<LONG>(std::roundf(originX + width)),
static_cast<LONG>(std::roundf(originY + height)));
}
return rect;
}
}

View File

@@ -0,0 +1,229 @@
#pragma once
#include <Windows.h>
#include <algorithm>
namespace Common
{
namespace Display
{
namespace DPIAware
{
void InverseConvert(HMONITOR monitor_handle, float& width, float& height);
}
}
namespace Utils
{
namespace ProcessPath
{
// Get the executable path or module name for modern apps
inline std::wstring get_process_path(DWORD pid) noexcept
{
auto process = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, TRUE, pid);
std::wstring name;
if (process != INVALID_HANDLE_VALUE)
{
name.resize(MAX_PATH);
DWORD name_length = static_cast<DWORD>(name.length());
if (QueryFullProcessImageNameW(process, 0, name.data(), &name_length) == 0)
{
name_length = 0;
}
name.resize(name_length);
CloseHandle(process);
}
return name;
}
// Get the executable path or module name for modern apps
inline std::wstring get_process_path(HWND window) noexcept
{
const static std::wstring app_frame_host = L"ApplicationFrameHost.exe";
DWORD pid{};
GetWindowThreadProcessId(window, &pid);
auto name = get_process_path(pid);
if (name.length() >= app_frame_host.length() &&
name.compare(name.length() - app_frame_host.length(), app_frame_host.length(), app_frame_host) == 0)
{
// It is a UWP app. We will enumerate the windows and look for one created
// by something with a different PID
DWORD new_pid = pid;
EnumChildWindows(
window, [](HWND hwnd, LPARAM param) -> BOOL {
auto new_pid_ptr = reinterpret_cast<DWORD*>(param);
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
if (pid != *new_pid_ptr)
{
*new_pid_ptr = pid;
return FALSE;
}
else
{
return TRUE;
}
},
reinterpret_cast<LPARAM>(&new_pid));
// If we have a new pid, get the new name.
if (new_pid != pid)
{
return get_process_path(new_pid);
}
}
return name;
}
inline std::wstring get_process_path_waiting_uwp(HWND window)
{
const static std::wstring appFrameHost = L"ApplicationFrameHost.exe";
int attempt = 0;
auto processPath = get_process_path(window);
while (++attempt < 30 && processPath.length() >= appFrameHost.length() &&
processPath.compare(processPath.length() - appFrameHost.length(), appFrameHost.length(), appFrameHost) == 0)
{
std::this_thread::sleep_for(std::chrono::milliseconds(5));
processPath = get_process_path(window);
}
return processPath;
}
}
namespace ExcludedApps
{
inline bool find_folder_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
{
for (const auto& row : what)
{
const auto pos = where.rfind(row);
if (pos != std::wstring::npos)
{
return true;
}
}
return false;
}
// Checks if a process path is included in a list of strings.
inline bool find_app_name_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
{
for (const auto& row : what)
{
const auto pos = where.rfind(row);
const auto last_slash = where.rfind('\\');
//Check that row occurs in where, and its last occurrence contains in itself the first character after the last backslash.
if (pos != std::wstring::npos && pos <= last_slash + 1 && pos + row.length() > last_slash)
{
return true;
}
}
return false;
}
inline bool check_excluded_app_with_title(const std::vector<std::wstring>& excludedApps, std::wstring title)
{
CharUpperBuffW(title.data(), static_cast<DWORD>(title.length()));
for (const auto& app : excludedApps)
{
if (title.contains(app))
{
return true;
}
}
return false;
}
inline bool check_excluded_app(const std::wstring& processPath, const std::wstring& title, const std::vector<std::wstring>& excludedApps)
{
bool res = find_app_name_in_path(processPath, excludedApps);
if (!res)
{
res = check_excluded_app_with_title(excludedApps, title);
}
return res;
}
}
namespace Window
{
// Check if window is part of the shell or the taskbar.
inline bool is_system_window(HWND hwnd, const char* class_name)
{
// We compare the HWND against HWND of the desktop and shell windows,
// we also filter out some window class names know to belong to the taskbar.
constexpr std::array system_classes = { "SysListView32", "WorkerW", "Shell_TrayWnd", "Shell_SecondaryTrayWnd", "Progman" };
const std::array system_hwnds = { GetDesktopWindow(), GetShellWindow() };
for (auto system_hwnd : system_hwnds)
{
if (hwnd == system_hwnd)
{
return true;
}
}
for (const auto system_class : system_classes)
{
if (!strcmp(system_class, class_name))
{
return true;
}
}
return false;
}
}
}
}
// FancyZones WindowUtils
namespace WindowUtils
{
bool IsRoot(HWND window) noexcept;
bool IsMaximized(HWND window) noexcept;
constexpr bool HasStyle(LONG style, LONG styleToCheck) noexcept
{
return ((style & styleToCheck) == styleToCheck);
}
bool IsExcludedByDefault(HWND window, const std::wstring& processPath, const std::wstring& title);
RECT GetWindowRect(HWND window);
}
// addition for Projects
namespace WindowUtils
{
inline bool IsMinimized(HWND window)
{
return IsIconic(window);
}
#define MAX_TITLE_LENGTH 255
inline std::wstring GetWindowTitle(HWND window)
{
WCHAR title[MAX_TITLE_LENGTH];
int len = GetWindowTextW(window, title, MAX_TITLE_LENGTH);
if (len <= 0)
{
return {};
}
return std::wstring(title);
}
/*bool IsFullscreen(HWND window)
{
TODO
}*/
}

View File

@@ -0,0 +1,136 @@
#include "pch.h"
#include <chrono>
#include <iostream>
#include "../projects-common/Data.h"
#include "../projects-common/GuidUtils.h"
#include "../projects-common/WindowEnumerator.h"
#include "MonitorUtils.h"
#include "WindowFilter.h"
int main(int argc, char* argv[])
{
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
HRESULT comInitHres = CoInitializeEx(0, COINIT_MULTITHREADED);
if (FAILED(comInitHres))
{
std::wcout << L"Failed to initialize COM library. " << comInitHres << std::endl;
return -1;
}
std::wstring fileName = JsonUtils::ProjectsFile();
if (argc > 1)
{
std::string fileNameParam = argv[1];
std::wstring filenameStr(fileNameParam.begin(), fileNameParam.end());
fileName = filenameStr;
}
// read previously saved projects
std::vector<Project> projects;
try
{
auto savedProjectsJson = json::from_file(fileName);
if (savedProjectsJson.has_value())
{
auto savedProjects = JsonUtils::ProjectsListJSON::FromJson(savedProjectsJson.value());
if (savedProjects.has_value())
{
projects = savedProjects.value();
}
}
}
catch (std::exception)
{
}
// new project name
std::wstring defaultNamePrefix = L"Project"; // TODO: localizable
int nextProjectIndex = 0;
for (const auto& proj : projects)
{
const std::wstring& name = proj.name;
if (name.starts_with(defaultNamePrefix))
{
try
{
int index = std::stoi(name.substr(defaultNamePrefix.length() + 1));
if (nextProjectIndex < index)
{
nextProjectIndex = index;
}
}
catch (std::exception) {}
}
}
std::wstring projectName = defaultNamePrefix + L" " + std::to_wstring(nextProjectIndex + 1);
time_t creationTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
Project project{ .id = CreateGuidString(), .name = projectName, .creationTime = creationTime };
// save monitor configuration
project.monitors = MonitorUtils::IdentifyMonitors();
// get list of windows
auto windows = WindowEnumerator::Enumerate(WindowFilter::Filter);
for (const auto& window : windows)
{
// filter by window rect size
RECT rect = WindowUtils::GetWindowRect(window);
if (rect.right - rect.left <= 0 || rect.bottom - rect.top <= 0)
{
continue;
}
// filter by window title
std::wstring title = WindowUtils::GetWindowTitle(window);
if (title.empty())
{
continue;
}
// filter by app path
std::wstring processPath = Common::Utils::ProcessPath::get_process_path_waiting_uwp(window);
if (WindowUtils::IsExcludedByDefault(window, processPath, title))
{
continue;
}
auto windowMonitor = MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY);
int monitorNumber = 0;
for (const auto& monitor : project.monitors)
{
if (monitor.monitor == windowMonitor)
{
monitorNumber = monitor.number;
break;
}
}
Project::Application app {
.hwnd = window,
.appPath = processPath,
.appTitle = title,
.commandLineArgs = L"",
.isMinimized = WindowUtils::IsMinimized(window),
.isMaximized = WindowUtils::IsMaximized(window),
.position = Project::Application::Position {
.x = rect.left,
.y = rect.top,
.width = rect.right - rect.left,
.height = rect.bottom - rect.top,
},
.monitor = monitorNumber,
};
project.apps.push_back(app);
}
projects.push_back(project);
json::to_file(fileName, JsonUtils::ProjectsListJSON::ToJson(projects));
return 0;
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.220531.1" targetFramework="native" />
</packages>

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,3 @@
#pragma once
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>

View File

@@ -0,0 +1,405 @@
#pragma once
#include <string>
#include <optional>
#include <shlobj.h>
#include "json.h"
struct Project
{
struct Application
{
struct Position
{
int x;
int y;
int width;
int height;
RECT toRect() const noexcept
{
return RECT{.left = x, .top = y, .right = x + width, .bottom = y + height };
}
};
HWND hwnd{};
std::wstring appPath;
std::wstring appTitle;
std::wstring commandLineArgs;
bool isMinimized{};
bool isMaximized{};
Position position{};
int monitor{};
};
struct Monitor
{
struct MonitorRect
{
int top;
int left;
int width;
int height;
};
HMONITOR monitor{};
std::wstring id;
std::wstring instanceId;
unsigned int number{};
unsigned int dpi{};
MonitorRect monitorRectDpiAware{};
MonitorRect monitorRectDpiUnaware{};
};
std::wstring id;
std::wstring name;
time_t creationTime;
std::optional<time_t> lastLaunchedTime;
bool isShortcutNeeded;
std::vector<Monitor> monitors;
std::vector<Application> apps;
};
struct ProjectsList
{
std::vector<Project> projects;
};
namespace JsonUtils
{
inline std::wstring ProjectsFile()
{
wchar_t path[MAX_PATH + 1] = { 0 };
SHGetFolderPathW(NULL, CSIDL_DESKTOPDIRECTORY, NULL, 0, path);
return std::wstring(path) + L"\\projects.json";
}
namespace ProjectJSON
{
namespace ApplicationJSON
{
namespace PositionJSON
{
namespace NonLocalizable
{
const static wchar_t* XAxisID = L"X";
const static wchar_t* YAxisID = L"Y";
const static wchar_t* WidthID = L"width";
const static wchar_t* HeightID = L"height";
}
inline json::JsonObject ToJson(const Project::Application::Position& position)
{
json::JsonObject json{};
json.SetNamedValue(NonLocalizable::XAxisID, json::value(position.x));
json.SetNamedValue(NonLocalizable::YAxisID, json::value(position.y));
json.SetNamedValue(NonLocalizable::WidthID, json::value(position.width));
json.SetNamedValue(NonLocalizable::HeightID, json::value(position.height));
return json;
}
inline std::optional<Project::Application::Position> FromJson(const json::JsonObject& json)
{
Project::Application::Position result;
try
{
result.x = static_cast<int>(json.GetNamedNumber(NonLocalizable::XAxisID, 0));
result.y = static_cast<int>(json.GetNamedNumber(NonLocalizable::YAxisID, 0));
result.width = static_cast<int>(json.GetNamedNumber(NonLocalizable::WidthID, 0));
result.height = static_cast<int>(json.GetNamedNumber(NonLocalizable::HeightID, 0));
}
catch (const winrt::hresult_error&)
{
return std::nullopt;
}
return result;
}
}
namespace NonLocalizable
{
const static wchar_t* AppPathID = L"application";
const static wchar_t* HwndID = L"hwnd";
const static wchar_t* AppTitleID = L"title";
const static wchar_t* CommandLineArgsID = L"command-line-arguments";
const static wchar_t* MinimizedID = L"minimized";
const static wchar_t* MaximizedID = L"maximized";
const static wchar_t* PositionID = L"position";
const static wchar_t* MonitorID = L"monitor";
}
inline json::JsonObject ToJson(const Project::Application& data)
{
json::JsonObject json{};
json.SetNamedValue(NonLocalizable::HwndID, json::value(static_cast<double>(reinterpret_cast<long long>(data.hwnd))));
json.SetNamedValue(NonLocalizable::AppPathID, json::value(data.appPath));
json.SetNamedValue(NonLocalizable::AppTitleID, json::value(data.appTitle));
json.SetNamedValue(NonLocalizable::CommandLineArgsID, json::value(data.commandLineArgs));
json.SetNamedValue(NonLocalizable::MinimizedID, json::value(data.isMinimized));
json.SetNamedValue(NonLocalizable::MaximizedID, json::value(data.isMaximized));
json.SetNamedValue(NonLocalizable::PositionID, PositionJSON::ToJson(data.position));
json.SetNamedValue(NonLocalizable::MonitorID, json::value(data.monitor));
return json;
}
inline std::optional<Project::Application> FromJson(const json::JsonObject& json)
{
Project::Application result;
try
{
result.hwnd = reinterpret_cast<HWND>(static_cast<long long>(json.GetNamedNumber(NonLocalizable::HwndID)));
result.appPath = json.GetNamedString(NonLocalizable::AppPathID);
result.appTitle = json.GetNamedString(NonLocalizable::AppTitleID);
result.commandLineArgs = json.GetNamedString(NonLocalizable::CommandLineArgsID);
result.isMaximized = json.GetNamedBoolean(NonLocalizable::MaximizedID);
result.isMinimized = json.GetNamedBoolean(NonLocalizable::MinimizedID);
result.monitor = static_cast<int>(json.GetNamedNumber(NonLocalizable::MonitorID));
if (json.HasKey(NonLocalizable::PositionID))
{
auto position = PositionJSON::FromJson(json.GetNamedObject(NonLocalizable::PositionID));
if (!position.has_value())
{
return std::nullopt;
}
result.position = position.value();
}
}
catch (const winrt::hresult_error&)
{
return std::nullopt;
}
return result;
}
}
namespace MonitorJSON
{
namespace MonitorRectJSON
{
namespace NonLocalizable
{
const static wchar_t* TopID = L"top";
const static wchar_t* LeftID = L"left";
const static wchar_t* WidthID = L"width";
const static wchar_t* HeightID = L"height";
}
inline json::JsonObject ToJson(const Project::Monitor::MonitorRect& data)
{
json::JsonObject json{};
json.SetNamedValue(NonLocalizable::TopID, json::value(data.top));
json.SetNamedValue(NonLocalizable::LeftID, json::value(data.left));
json.SetNamedValue(NonLocalizable::WidthID, json::value(data.width));
json.SetNamedValue(NonLocalizable::HeightID, json::value(data.height));
return json;
}
inline std::optional<Project::Monitor::MonitorRect> FromJson(const json::JsonObject& json)
{
Project::Monitor::MonitorRect result;
try
{
result.top = static_cast<int>(json.GetNamedNumber(NonLocalizable::TopID));
result.left = static_cast<int>(json.GetNamedNumber(NonLocalizable::LeftID));
result.width = static_cast<int>(json.GetNamedNumber(NonLocalizable::WidthID));
result.height = static_cast<int>(json.GetNamedNumber(NonLocalizable::HeightID));
}
catch (const winrt::hresult_error&)
{
return std::nullopt;
}
return result;
}
}
namespace NonLocalizable
{
const static wchar_t* MonitorID = L"id";
const static wchar_t* InstanceID = L"instance-id";
const static wchar_t* NumberID = L"monitor-number";
const static wchar_t* DpiID = L"dpi";
const static wchar_t* MonitorRectDpiAwareID = L"monitor-rect-dpi-aware";
const static wchar_t* MonitorRectDpiUnawareID = L"monitor-rect-dpi-unaware";
}
inline json::JsonObject ToJson(const Project::Monitor& data)
{
json::JsonObject json{};
json.SetNamedValue(NonLocalizable::MonitorID, json::value(data.id));
json.SetNamedValue(NonLocalizable::InstanceID, json::value(data.instanceId));
json.SetNamedValue(NonLocalizable::NumberID, json::value(data.number));
json.SetNamedValue(NonLocalizable::DpiID, json::value(data.dpi));
json.SetNamedValue(NonLocalizable::MonitorRectDpiAwareID, MonitorRectJSON::ToJson(data.monitorRectDpiAware));
json.SetNamedValue(NonLocalizable::MonitorRectDpiUnawareID, MonitorRectJSON::ToJson(data.monitorRectDpiUnaware));
return json;
}
inline std::optional<Project::Monitor> FromJson(const json::JsonObject& json)
{
Project::Monitor result;
try
{
result.id = json.GetNamedString(NonLocalizable::MonitorID);
result.instanceId = json.GetNamedString(NonLocalizable::InstanceID);
result.number = static_cast<int>(json.GetNamedNumber(NonLocalizable::NumberID));
result.dpi = static_cast<int>(json.GetNamedNumber(NonLocalizable::DpiID));
auto rectDpiAware = MonitorRectJSON::FromJson(json.GetNamedObject(NonLocalizable::MonitorRectDpiAwareID));
if (!rectDpiAware.has_value())
{
return std::nullopt;
}
auto rectDpiUnaware = MonitorRectJSON::FromJson(json.GetNamedObject(NonLocalizable::MonitorRectDpiUnawareID));
if (!rectDpiUnaware.has_value())
{
return std::nullopt;
}
result.monitorRectDpiAware = rectDpiAware.value();
result.monitorRectDpiUnaware = rectDpiUnaware.value();
}
catch (const winrt::hresult_error&)
{
return std::nullopt;
}
return result;
}
}
namespace NonLocalizable
{
const static wchar_t* IdID = L"id";
const static wchar_t* NameID = L"name";
const static wchar_t* CreationTimeID = L"creation-time";
const static wchar_t* LastLaunchedTimeID = L"last-launched-time";
const static wchar_t* IsShortcutNeededID = L"is-shortcut-needed";
const static wchar_t* MonitorConfigurationID = L"monitor-configuration";
const static wchar_t* AppsID = L"applications";
}
inline json::JsonObject ToJson(const Project& data)
{
json::JsonObject json{};
json::JsonArray appsArray{};
for (const auto& app : data.apps)
{
appsArray.Append(ApplicationJSON::ToJson(app));
}
json::JsonArray monitorsArray{};
for (const auto& monitor : data.monitors)
{
monitorsArray.Append(MonitorJSON::ToJson(monitor));
}
json.SetNamedValue(NonLocalizable::IdID, json::value(data.id));
json.SetNamedValue(NonLocalizable::NameID, json::value(data.name));
json.SetNamedValue(NonLocalizable::CreationTimeID, json::value(static_cast<long>(data.creationTime)));
if (data.lastLaunchedTime.has_value())
{
json.SetNamedValue(NonLocalizable::LastLaunchedTimeID, json::value(static_cast<long>(data.lastLaunchedTime.value())));
}
json.SetNamedValue(NonLocalizable::IsShortcutNeededID, json::value(data.isShortcutNeeded));
json.SetNamedValue(NonLocalizable::MonitorConfigurationID, monitorsArray);
json.SetNamedValue(NonLocalizable::AppsID, appsArray);
return json;
}
inline std::optional<Project> FromJson(const json::JsonObject& json)
{
Project result{};
try
{
result.id = json.GetNamedString(NonLocalizable::IdID);
result.name = json.GetNamedString(NonLocalizable::NameID);
result.creationTime = static_cast<time_t>(json.GetNamedNumber(NonLocalizable::CreationTimeID));
if (json.HasKey(NonLocalizable::LastLaunchedTimeID))
{
result.lastLaunchedTime = static_cast<time_t>(json.GetNamedNumber(NonLocalizable::LastLaunchedTimeID));
}
result.isShortcutNeeded = json.GetNamedBoolean(NonLocalizable::IsShortcutNeededID);
auto appsArray = json.GetNamedArray(NonLocalizable::AppsID);
for (uint32_t i = 0; i < appsArray.Size(); ++i)
{
if (auto obj = ApplicationJSON::FromJson(appsArray.GetObjectAt(i)); obj.has_value())
{
result.apps.push_back(obj.value());
}
}
auto monitorsArray = json.GetNamedArray(NonLocalizable::MonitorConfigurationID);
for (uint32_t i = 0; i < monitorsArray.Size(); ++i)
{
if (auto obj = MonitorJSON::FromJson(monitorsArray.GetObjectAt(i)); obj.has_value())
{
result.monitors.push_back(obj.value());
}
}
}
catch (const winrt::hresult_error&)
{
return std::nullopt;
}
return result;
}
}
namespace ProjectsListJSON
{
namespace NonLocalizable
{
const static wchar_t* ProjectsID = L"projects";
}
inline json::JsonObject ToJson(const std::vector<Project>& data)
{
json::JsonObject json{};
json::JsonArray projectsArray{};
for (const auto& project : data)
{
projectsArray.Append(ProjectJSON::ToJson(project));
}
json.SetNamedValue(NonLocalizable::ProjectsID, projectsArray);
return json;
}
inline std::optional<std::vector<Project>> FromJson(const json::JsonObject& json)
{
std::vector<Project> result{};
try
{
auto array = json.GetNamedArray(NonLocalizable::ProjectsID);
for (uint32_t i = 0; i < array.Size(); ++i)
{
if (auto obj = ProjectJSON::FromJson(array.GetObjectAt(i)); obj.has_value())
{
result.push_back(obj.value());
}
}
}
catch (const winrt::hresult_error&)
{
return std::nullopt;
}
return result;
}
}
}

View File

@@ -0,0 +1,34 @@
inline std::optional<GUID> GuidFromString(const std::wstring& str) noexcept
{
GUID id;
if (SUCCEEDED(CLSIDFromString(str.c_str(), &id)))
{
return id;
}
return std::nullopt;
}
inline std::wstring GuidToString(const GUID& guid) noexcept
{
OLECHAR* guidString;
StringFromCLSID(guid, &guidString);
std::wstring guidWString(guidString);
::CoTaskMemFree(guidString);
return guidWString;
}
inline std::wstring CreateGuidString()
{
GUID guid;
if (CoCreateGuid(&guid) == S_OK)
{
return GuidToString(guid);
}
return L"";
}

View File

@@ -0,0 +1,35 @@
#pragma once
#include <functional>
#include <vector>
#include <Windows.h>
class MonitorEnumerator
{
public:
static std::vector<std::pair<HMONITOR, MONITORINFOEX>> Enumerate()
{
MonitorEnumerator inst;
EnumDisplayMonitors(NULL, NULL, Callback, reinterpret_cast<LPARAM>(&inst));
return inst.m_monitors;
}
private:
MonitorEnumerator() = default;
~MonitorEnumerator() = default;
static BOOL CALLBACK Callback(HMONITOR monitor, HDC /*hdc*/, LPRECT /*pRect*/, LPARAM param)
{
MonitorEnumerator* inst = reinterpret_cast<MonitorEnumerator*>(param);
MONITORINFOEX mi;
mi.cbSize = sizeof(mi);
if (GetMonitorInfo(monitor, &mi))
{
inst->m_monitors.push_back({monitor, mi});
}
return TRUE;
}
std::vector<std::pair<HMONITOR, MONITORINFOEX>> m_monitors;
};

View File

@@ -0,0 +1,35 @@
#pragma once
#include <functional>
#include <vector>
#include <Windows.h>
class WindowEnumerator
{
public:
static std::vector<HWND> Enumerate(const std::function<bool(HWND)>& filter)
{
WindowEnumerator inst;
inst.m_filter = filter;
EnumWindows(Callback, reinterpret_cast<LPARAM>(&inst));
return inst.m_windows;
}
private:
WindowEnumerator() = default;
~WindowEnumerator() = default;
static BOOL CALLBACK Callback(HWND window, LPARAM data)
{
WindowEnumerator* inst = reinterpret_cast<WindowEnumerator*>(data);
if (inst->m_filter(window))
{
inst->m_windows.push_back(window);
}
return TRUE;
}
std::vector<HWND> m_windows;
std::function<bool(HWND)> m_filter = [](HWND) { return true; };
};

View File

@@ -0,0 +1,110 @@
#pragma once
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Data.Json.h>
#include <optional>
#include <fstream>
// common/utils/json.h
namespace json
{
using namespace winrt::Windows::Data::Json;
inline std::optional<JsonObject> from_file(std::wstring_view file_name)
{
try
{
std::ifstream file(file_name.data(), std::ios::binary);
if (file.is_open())
{
using isbi = std::istreambuf_iterator<char>;
std::string obj_str{ isbi{ file }, isbi{} };
return JsonValue::Parse(winrt::to_hstring(obj_str)).GetObjectW();
}
return std::nullopt;
}
catch (...)
{
return std::nullopt;
}
}
inline void to_file(std::wstring_view file_name, const JsonObject& obj)
{
std::wstring obj_str{ obj.Stringify().c_str() };
std::ofstream{ file_name.data(), std::ios::binary } << winrt::to_string(obj_str);
}
inline bool has(
const json::JsonObject& o,
std::wstring_view name,
const json::JsonValueType type = JsonValueType::Object)
{
return o.HasKey(name) && o.GetNamedValue(name).ValueType() == type;
}
template<typename T>
inline std::enable_if_t<std::is_arithmetic_v<T>, JsonValue> value(const T arithmetic)
{
return json::JsonValue::CreateNumberValue(arithmetic);
}
template<typename T>
inline std::enable_if_t<!std::is_arithmetic_v<T>, JsonValue> value(T s)
{
return json::JsonValue::CreateStringValue(s);
}
inline JsonValue value(const bool boolean)
{
return json::JsonValue::CreateBooleanValue(boolean);
}
inline JsonValue value(JsonObject value)
{
return value.as<JsonValue>();
}
inline JsonValue value(JsonValue value)
{
return value; // identity function overload for convenience
}
template<typename T, typename D = std::optional<T>>
requires std::constructible_from<std::optional<T>, D>
void get(const json::JsonObject& o, const wchar_t* name, T& destination, D default_value = std::nullopt)
{
try
{
if constexpr (std::is_same_v<T, bool>)
{
destination = o.GetNamedBoolean(name);
}
else if constexpr (std::is_arithmetic_v<T>)
{
destination = static_cast<T>(o.GetNamedNumber(name));
}
else if constexpr (std::is_same_v<T, std::wstring>)
{
destination = o.GetNamedString(name);
}
else if constexpr (std::is_same_v<T, json::JsonObject>)
{
destination = o.GetNamedObject(name);
}
else
{
static_assert(std::bool_constant<std::is_same_v<T, T&>>::value, "Unsupported type");
}
}
catch (...)
{
std::optional<T> maybe_default{ std::move(default_value) };
if (maybe_default.has_value())
destination = std::move(*maybe_default);
}
}
}