[Workspaces] implement the move feature (#35480)

* [Workspaces] Add move functionality

* spell checker

* [Workspaces] Modify Arranger to move apps without launch

* moved ipc helper

* removed callback

* use LauncherStatus in WindowArranger

* wait for launching next app

* launch in a separate thread and protect by mutexes

* update app version in advance

* changed canceling launch

* increased waiting time

* Fix optional parameter load from json

* changed arranger waiting time

* additional waiting time for Outlook

* added app id

* ensure ids before launch

* set id in editor

* minor updates

* [Workspaces] Move: Get the nearest window when moving a window

* [Workspaces] convert optional boolean to enum to avoid json problems

* Handle case when the new Application Property "moveIfExists" does not exist

* Re-implementing app-window pairing for moving feature.

* spell checker

* XAML formatting

* Fixing bug: IPC message not arriving

* spell checker

* Removing app-level-setting for move app. Also fixed compiler errors due styling.

* Updating editor window layout

* Re-implementing window positioning UI elements

* XAML formatting

* Code review findings

* Code cleanup

* Code cleanup

* Code cleanup

* code cleanup

* Code cleanup

* Code cleanup

* fix Move attribute after launch and snapshot

* Extend WindowArranger with PWA functionality to detect different PWA apps. PwaHelper moved to the common library

* fix repeat counter in the editor

* Code optimization

* code cleanup, optimization

* fix double-processing window

---------

Co-authored-by: Seraphima <zykovas91@gmail.com>
Co-authored-by: donlaci <donlaci@yahoo.com>
This commit is contained in:
Laszlo Nemeth 2024-12-04 18:17:54 +01:00 коммит произвёл GitHub
Родитель e0949cb4ea
Коммит 89be43e15d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
38 изменённых файлов: 670 добавлений и 558 удалений

Просмотреть файл

@ -109,6 +109,24 @@ namespace DPIAware
}
}
void InverseConvert(HMONITOR monitor_handle, RECT& rect)
{
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)
{
rect.left = static_cast<long>(std::round(rect.left * static_cast<float>(DEFAULT_DPI) / dpi_x));
rect.right = static_cast<long>(std::round(rect.right * static_cast<float>(DEFAULT_DPI) / dpi_x));
rect.top = static_cast<long>(std::round(rect.top * static_cast<float>(DEFAULT_DPI) / dpi_y));
rect.bottom = static_cast<long>(std::round(rect.bottom * static_cast<float>(DEFAULT_DPI) / dpi_y));
}
}
void EnableDPIAwarenessForThisProcess()
{
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

Просмотреть файл

@ -15,6 +15,7 @@ namespace DPIAware
void Convert(HMONITOR monitor_handle, RECT& rect);
void ConvertByCursorPosition(float& width, float& height);
void InverseConvert(HMONITOR monitor_handle, float& width, float& height);
void InverseConvert(HMONITOR monitor_handle, RECT& rect);
void EnableDPIAwarenessForThisProcess();
enum AwarenessLevel

Просмотреть файл

@ -3,9 +3,6 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Workspaces.Data;
using static WorkspacesEditor.Data.ProjectData;
namespace WorkspacesEditor.Data

Просмотреть файл

@ -8,13 +8,7 @@ namespace WorkspacesEditor.Data
{
public class TempProjectData : ProjectData
{
public static string File
{
get
{
return FolderUtils.DataFolder() + "\\temp-workspaces.json";
}
}
public static string File => FolderUtils.DataFolder() + "\\temp-workspaces.json";
public static void DeleteTempFile()
{

Просмотреть файл

@ -3,8 +3,6 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Workspaces.Data;
using WorkspacesEditor.Utils;
using static WorkspacesEditor.Data.ProjectData;
@ -14,13 +12,7 @@ namespace WorkspacesEditor.Data
{
public class WorkspacesData : WorkspacesEditorData<WorkspacesListWrapper>
{
public string File
{
get
{
return FolderUtils.DataFolder() + "\\workspaces.json";
}
}
public string File => FolderUtils.DataFolder() + "\\workspaces.json";
public struct WorkspacesListWrapper
{

Просмотреть файл

@ -3,28 +3,24 @@
// See the LICENSE file in the project root for more information.
using System.Text.Json;
using WorkspacesEditor.Utils;
namespace Workspaces.Data
namespace WorkspacesEditor.Data
{
public class WorkspacesEditorData<T>
{
protected JsonSerializerOptions JsonOptions
{
get
get => new()
{
return new JsonSerializerOptions
{
PropertyNamingPolicy = new DashCaseNamingPolicy(),
WriteIndented = true,
};
}
PropertyNamingPolicy = new DashCaseNamingPolicy(),
WriteIndented = true,
};
}
public T Read(string file)
{
IOUtils ioUtils = new IOUtils();
IOUtils ioUtils = new();
string data = ioUtils.ReadFile(file);
return JsonSerializer.Deserialize<T>(data, JsonOptions);
}

Просмотреть файл

@ -8,7 +8,7 @@
xmlns:ui="http://schemas.modernwpf.com/2019"
x:Name="WorkspacesMainWindow"
Title="{x:Static props:Resources.MainTitle}"
MinWidth="700"
MinWidth="750"
MinHeight="680"
ui:TitleBar.Background="{DynamicResource PrimaryBackgroundBrush}"
ui:TitleBar.InactiveBackground="{DynamicResource TertiaryBackgroundBrush}"

Просмотреть файл

@ -18,14 +18,7 @@ namespace WorkspacesEditor.Models
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
if (item is MonitorHeaderRow)
{
return HeaderTemplate;
}
else
{
return AppTemplate;
}
return item is MonitorHeaderRow ? HeaderTemplate : AppTemplate;
}
}
}

Просмотреть файл

@ -1,25 +1,22 @@
// Copyright (c) Microsoft Corporation
// 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;
using ManagedCommon;
using Windows.Management.Deployment;
using WorkspacesCsharpLibrary;
using WorkspacesCsharpLibrary.Models;
namespace WorkspacesEditor.Models
{
public enum WindowPositionKind
{
Custom = 0,
Maximized = 1,
Minimized = 2,
}
public class Application : BaseApplication, IDisposable
{
private bool _isInitialized;
@ -79,7 +76,7 @@ namespace WorkspacesEditor.Models
return left.X != right.X || left.Y != right.Y || left.Width != right.Width || left.Height != right.Height;
}
public override bool Equals(object obj)
public override readonly bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
@ -90,7 +87,7 @@ namespace WorkspacesEditor.Models
return X == pos.X && Y == pos.Y && Width == pos.Width && Height == pos.Height;
}
public override int GetHashCode()
public override readonly int GetHashCode()
{
return base.GetHashCode();
}
@ -136,36 +133,24 @@ namespace WorkspacesEditor.Models
}
}
private bool _minimized;
public bool Minimized { get; set; }
public bool Minimized
public bool Maximized { get; set; }
public bool EditPositionEnabled => !Minimized && !Maximized;
public int PositionComboboxIndex
{
get => _minimized;
get => Maximized ? (int)WindowPositionKind.Maximized : Minimized ? (int)WindowPositionKind.Minimized : (int)WindowPositionKind.Custom;
set
{
_minimized = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Minimized)));
Maximized = value == (int)WindowPositionKind.Maximized;
Minimized = value == (int)WindowPositionKind.Minimized;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(EditPositionEnabled)));
RedrawPreviewImage();
}
}
private bool _maximized;
public bool Maximized
{
get => _maximized;
set
{
_maximized = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Maximized)));
OnPropertyChanged(new PropertyChangedEventArgs(nameof(EditPositionEnabled)));
RedrawPreviewImage();
}
}
public bool EditPositionEnabled { get => !Minimized && !Maximized; }
private string _appMainParams;
public string AppMainParams
@ -183,7 +168,7 @@ namespace WorkspacesEditor.Models
}
}
public bool IsAppMainParamVisible { get => !string.IsNullOrWhiteSpace(_appMainParams); }
public bool IsAppMainParamVisible => !string.IsNullOrWhiteSpace(_appMainParams);
[JsonIgnore]
public bool IsHighlighted { get; set; }
@ -192,13 +177,7 @@ namespace WorkspacesEditor.Models
public int RepeatIndex { get; set; }
[JsonIgnore]
public string RepeatIndexString
{
get
{
return RepeatIndex <= 1 ? string.Empty : RepeatIndex.ToString(CultureInfo.InvariantCulture);
}
}
public string RepeatIndexString => RepeatIndex <= 1 ? string.Empty : RepeatIndex.ToString(CultureInfo.InvariantCulture);
private WindowPosition _position;
@ -242,10 +221,7 @@ namespace WorkspacesEditor.Models
{
get
{
if (_monitorSetup == null)
{
_monitorSetup = Parent.GetMonitorForApp(this);
}
_monitorSetup ??= Parent.GetMonitorForApp(this);
return _monitorSetup;
}
@ -271,7 +247,7 @@ namespace WorkspacesEditor.Models
}
}
public string DeleteButtonContent { get => _isIncluded ? Properties.Resources.Delete : Properties.Resources.AddBack; }
public string DeleteButtonContent => _isIncluded ? Properties.Resources.Delete : Properties.Resources.AddBack;
private bool _isIncluded = true;
@ -298,15 +274,5 @@ namespace WorkspacesEditor.Models
CommandLineArguments = newCommandLineValue;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(AppMainParams)));
}
internal void MaximizedChecked()
{
Minimized = false;
}
internal void MinimizedChecked()
{
Maximized = false;
}
}
}

Просмотреть файл

@ -6,28 +6,18 @@ using System.Windows;
namespace WorkspacesEditor.Models
{
public class Monitor
public class Monitor(string monitorName, string monitorInstanceId, int number, int dpi, Rect dpiAwareBounds, Rect dpiUnawareBounds)
{
public string MonitorName { get; private set; }
public string MonitorName { get; private set; } = monitorName;
public string MonitorInstanceId { get; private set; }
public string MonitorInstanceId { get; private set; } = monitorInstanceId;
public int MonitorNumber { get; private set; }
public int MonitorNumber { get; private set; } = number;
public int Dpi { get; private set; }
public int Dpi { get; private set; } = dpi;
public Rect MonitorDpiUnawareBounds { get; private set; }
public Rect MonitorDpiUnawareBounds { get; private set; } = dpiUnawareBounds;
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;
}
public Rect MonitorDpiAwareBounds { get; private set; } = dpiAwareBounds;
}
}

Просмотреть файл

@ -16,9 +16,9 @@ namespace WorkspacesEditor.Models
PropertyChanged?.Invoke(this, e);
}
public string MonitorInfo { get => MonitorName; }
public string MonitorInfo => MonitorName;
public string MonitorInfoWithResolution { get => $"{MonitorName} {MonitorDpiAwareBounds.Width}x{MonitorDpiAwareBounds.Height}"; }
public string MonitorInfoWithResolution => $"{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)

Просмотреть файл

@ -29,10 +29,7 @@ namespace WorkspacesEditor.Models
public string Name
{
get
{
return _name;
}
get => _name;
set
{
@ -68,8 +65,7 @@ namespace WorkspacesEditor.Models
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;
TimeSpan ts = DateTime.UtcNow - lastLaunchDateTime;
double delta = Math.Abs(ts.TotalSeconds);
if (delta < 1 * MINUTE)
@ -120,10 +116,7 @@ namespace WorkspacesEditor.Models
}
}
public bool CanBeSaved
{
get => Name.Length > 0 && Applications.Count > 0;
}
public bool CanBeSaved => Name.Length > 0 && Applications.Count > 0;
private bool _isRevertEnabled;
@ -145,10 +138,7 @@ namespace WorkspacesEditor.Models
[JsonIgnore]
public bool IsPopupVisible
{
get
{
return _isPopupVisible;
}
get => _isPopupVisible;
set
{
@ -163,11 +153,11 @@ namespace WorkspacesEditor.Models
{
get
{
List<object> applicationsListed = new List<object>();
List<object> applicationsListed = [];
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))
foreach (IGrouping<MonitorSetup, Application> appItem in apps.OrderBy(x => x.Key.MonitorDpiUnawareBounds.Left).ThenBy(x => x.Key.MonitorDpiUnawareBounds.Top))
{
MonitorHeaderRow headerRow = new MonitorHeaderRow { MonitorName = "Screen " + appItem.Key.MonitorNumber, SelectString = Properties.Resources.SelectAllAppsOnMonitor + " " + appItem.Key.MonitorInfo };
MonitorHeaderRow headerRow = new() { MonitorName = "Screen " + appItem.Key.MonitorNumber, SelectString = Properties.Resources.SelectAllAppsOnMonitor + " " + appItem.Key.MonitorInfo };
applicationsListed.Add(headerRow);
foreach (Application app in appItem)
{
@ -175,10 +165,10 @@ namespace WorkspacesEditor.Models
}
}
var minimizedApps = Applications.Where(x => x.Minimized);
IEnumerable<Application> minimizedApps = Applications.Where(x => x.Minimized);
if (minimizedApps.Any())
{
MonitorHeaderRow headerRow = new MonitorHeaderRow { MonitorName = Properties.Resources.Minimized_Apps, SelectString = Properties.Resources.SelectAllMinimizedApps };
MonitorHeaderRow headerRow = new() { MonitorName = Properties.Resources.Minimized_Apps, SelectString = Properties.Resources.SelectAllMinimizedApps };
applicationsListed.Add(headerRow);
foreach (Application app in minimizedApps)
{
@ -219,17 +209,17 @@ namespace WorkspacesEditor.Models
int screenIndex = 1;
Monitors = new List<MonitorSetup>();
foreach (var item in selectedProject.Monitors.OrderBy(x => x.MonitorDpiAwareBounds.Left).ThenBy(x => x.MonitorDpiAwareBounds.Top))
Monitors = [];
foreach (MonitorSetup item in selectedProject.Monitors.OrderBy(x => x.MonitorDpiAwareBounds.Left).ThenBy(x => x.MonitorDpiAwareBounds.Top))
{
Monitors.Add(item);
screenIndex++;
}
Applications = new List<Application>();
foreach (var item in selectedProject.Applications)
Applications = [];
foreach (Application item in selectedProject.Applications)
{
Application newApp = new Application(item);
Application newApp = new(item);
newApp.Parent = this;
newApp.InitializationFinished();
Applications.Add(newApp);
@ -244,14 +234,14 @@ namespace WorkspacesEditor.Models
LastLaunchedTime = project.LastLaunchedTime;
IsShortcutNeeded = project.IsShortcutNeeded;
MoveExistingWindows = project.MoveExistingWindows;
Monitors = new List<MonitorSetup>() { };
Applications = new List<Models.Application> { };
Monitors = [];
Applications = [];
foreach (var app in project.Applications)
foreach (ProjectData.ApplicationWrapper app in project.Applications)
{
Models.Application newApp = new Models.Application()
Models.Application newApp = new()
{
Id = string.IsNullOrEmpty(app.Id) ? $"{{{Guid.NewGuid().ToString()}}}" : app.Id,
Id = string.IsNullOrEmpty(app.Id) ? $"{{{Guid.NewGuid()}}}" : app.Id,
AppName = app.Application,
AppPath = app.ApplicationPath,
AppTitle = app.Title,
@ -278,20 +268,17 @@ namespace WorkspacesEditor.Models
Applications.Add(newApp);
}
foreach (var monitor in project.MonitorConfiguration)
foreach (ProjectData.MonitorConfigurationWrapper monitor in project.MonitorConfiguration)
{
System.Windows.Rect dpiAware = new System.Windows.Rect(monitor.MonitorRectDpiAware.Left, monitor.MonitorRectDpiAware.Top, monitor.MonitorRectDpiAware.Width, monitor.MonitorRectDpiAware.Height);
System.Windows.Rect dpiUnaware = new System.Windows.Rect(monitor.MonitorRectDpiUnaware.Left, monitor.MonitorRectDpiUnaware.Top, monitor.MonitorRectDpiUnaware.Width, monitor.MonitorRectDpiUnaware.Height);
System.Windows.Rect dpiAware = new(monitor.MonitorRectDpiAware.Left, monitor.MonitorRectDpiAware.Top, monitor.MonitorRectDpiAware.Width, monitor.MonitorRectDpiAware.Height);
System.Windows.Rect dpiUnaware = new(monitor.MonitorRectDpiUnaware.Left, monitor.MonitorRectDpiUnaware.Top, monitor.MonitorRectDpiUnaware.Width, monitor.MonitorRectDpiUnaware.Height);
Monitors.Add(new MonitorSetup(monitor.Id, monitor.InstanceId, monitor.MonitorNumber, monitor.Dpi, dpiAware, dpiUnaware));
}
}
public BitmapImage PreviewIcons
{
get
{
return _previewIcons;
}
get => _previewIcons;
set
{
@ -302,10 +289,7 @@ namespace WorkspacesEditor.Models
public BitmapImage PreviewImage
{
get
{
return _previewImage;
}
get => _previewImage;
set
{
@ -316,10 +300,7 @@ namespace WorkspacesEditor.Models
public double PreviewImageWidth
{
get
{
return _previewImageWidth;
}
get => _previewImageWidth;
set
{
@ -366,6 +347,7 @@ namespace WorkspacesEditor.Models
Id = other.Id;
Name = other.Name;
IsRevertEnabled = true;
MoveExistingWindows = other.MoveExistingWindows;
}
internal void CloseExpanders()
@ -378,13 +360,13 @@ namespace WorkspacesEditor.Models
internal MonitorSetup GetMonitorForApp(Application app)
{
var monitorSetup = Monitors.Where(x => x.MonitorNumber == app.MonitorNumber).FirstOrDefault();
MonitorSetup monitorSetup = Monitors.Where(x => x.MonitorNumber == app.MonitorNumber).FirstOrDefault();
if (monitorSetup == null)
{
// monitors changed: try to determine monitor id based on middle point
int middleX = app.Position.X + (app.Position.Width / 2);
int middleY = app.Position.Y + (app.Position.Height / 2);
var monitorCandidate = Monitors.Where(x =>
MonitorSetup monitorCandidate = Monitors.Where(x =>
(x.MonitorDpiUnawareBounds.Left < middleX) &&
(x.MonitorDpiUnawareBounds.Right > middleX) &&
(x.MonitorDpiUnawareBounds.Top < middleY) &&

Просмотреть файл

@ -61,7 +61,7 @@ namespace WorkspacesEditor.Properties {
}
/// <summary>
/// Looks up a localized string similar to Add Back.
/// Looks up a localized string similar to Add back.
/// </summary>
public static string AddBack {
get {
@ -78,15 +78,6 @@ namespace WorkspacesEditor.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Launch new app instances.
/// </summary>
public static string AlwaysLaunch {
get {
return ResourceManager.GetString("AlwaysLaunch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to app.
/// </summary>
@ -169,7 +160,7 @@ namespace WorkspacesEditor.Properties {
}
/// <summary>
/// Looks up a localized string similar to Create Desktop Shortcut.
/// Looks up a localized string similar to Create desktop shortcut.
/// </summary>
public static string CreateShortcut {
get {
@ -186,6 +177,15 @@ namespace WorkspacesEditor.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Custom.
/// </summary>
public static string Custom {
get {
return ResourceManager.GetString("Custom", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to days ago.
/// </summary>
@ -223,7 +223,7 @@ namespace WorkspacesEditor.Properties {
}
/// <summary>
/// Looks up a localized string similar to Remove Selected Apps.
/// Looks up a localized string similar to Remove selected apps.
/// </summary>
public static string DeleteSelected {
get {
@ -322,7 +322,7 @@ namespace WorkspacesEditor.Properties {
}
/// <summary>
/// Looks up a localized string similar to Launch &amp; Edit.
/// Looks up a localized string similar to Launch &amp; edit.
/// </summary>
public static string LaunchEdit {
get {
@ -367,7 +367,7 @@ namespace WorkspacesEditor.Properties {
}
/// <summary>
/// Looks up a localized string similar to Minimized Apps.
/// Looks up a localized string similar to Minimized apps.
/// </summary>
public static string Minimized_Apps {
get {
@ -394,7 +394,7 @@ namespace WorkspacesEditor.Properties {
}
/// <summary>
/// Looks up a localized string similar to Move apps if present.
/// Looks up a localized string similar to Move existing windows.
/// </summary>
public static string MoveIfExist {
get {
@ -502,7 +502,7 @@ namespace WorkspacesEditor.Properties {
}
/// <summary>
/// Looks up a localized string similar to Pin Workspaces to Taskbar.
/// Looks up a localized string similar to Pin Workspaces to taskbar.
/// </summary>
public static string PinToTaskbar {
get {
@ -565,7 +565,7 @@ namespace WorkspacesEditor.Properties {
}
/// <summary>
/// Looks up a localized string similar to Select All Apps on.
/// Looks up a localized string similar to Select all apps on.
/// </summary>
public static string SelectAllAppsOnMonitor {
get {
@ -574,7 +574,7 @@ namespace WorkspacesEditor.Properties {
}
/// <summary>
/// Looks up a localized string similar to Select All Minimized Apps.
/// Looks up a localized string similar to Select all minimized apps.
/// </summary>
public static string SelectAllMinimizedApps {
get {
@ -583,7 +583,7 @@ namespace WorkspacesEditor.Properties {
}
/// <summary>
/// Looks up a localized string similar to Select All Apps in Workspace.
/// Looks up a localized string similar to Select all apps in Workspace.
/// </summary>
public static string SelectedAllInWorkspace {
get {
@ -645,6 +645,15 @@ namespace WorkspacesEditor.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Window position.
/// </summary>
public static string WindowPosition {
get {
return ResourceManager.GetString("WindowPosition", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Workspace name.
/// </summary>

Просмотреть файл

@ -123,9 +123,6 @@
<data name="Admin" xml:space="preserve">
<value>Admin</value>
</data>
<data name="AlwaysLaunch" xml:space="preserve">
<value>Launch new app instances</value>
</data>
<data name="App" xml:space="preserve">
<value>app</value>
</data>
@ -160,6 +157,9 @@
<data name="CreateShortcut" xml:space="preserve">
<value>Create desktop shortcut</value>
</data>
<data name="Custom" xml:space="preserve">
<value>Custom</value>
</data>
<data name="DaysAgo" xml:space="preserve">
<value>days ago</value>
</data>
@ -231,7 +231,7 @@
<value>months ago</value>
</data>
<data name="MoveIfExist" xml:space="preserve">
<value>Move apps if present</value>
<value>Move existing windows</value>
</data>
<data name="Name" xml:space="preserve">
<value>Name</value>
@ -321,6 +321,9 @@
<data name="Width" xml:space="preserve">
<value>Width</value>
</data>
<data name="WindowPosition" xml:space="preserve">
<value>Window position</value>
</data>
<data name="WriteArgs" xml:space="preserve">
<value>Write arguments here</value>
</data>

Просмотреть файл

@ -20,21 +20,21 @@ namespace WorkspacesEditor.Utils
{
public class DrawHelper
{
private static Font font = new("Tahoma", 24);
private static double scale = 0.1;
private static readonly Font Font = new("Tahoma", 24);
private static readonly double Scale = 0.1;
private static double gapWidth;
private static double gapHeight;
public static BitmapImage DrawPreview(Project project, Rectangle bounds, Theme currentTheme)
{
List<double> horizontalGaps = new List<double>();
List<double> verticalGaps = new List<double>();
List<double> horizontalGaps = [];
List<double> verticalGaps = [];
gapWidth = bounds.Width * 0.01;
gapHeight = bounds.Height * 0.01;
int Scaled(double value)
{
return (int)(value * scale);
return (int)(value * Scale);
}
int TransformX(double posX)
@ -54,7 +54,7 @@ namespace WorkspacesEditor.Utils
if (app.Maximized)
{
Project project = app.Parent;
var monitor = project.GetMonitorForApp(app);
MonitorSetup monitor = project.GetMonitorForApp(app);
if (monitor == null)
{
// unrealistic case, there are no monitors at all in the workspace, use original rect
@ -69,22 +69,23 @@ namespace WorkspacesEditor.Utils
}
}
Dictionary<string, int> repeatCounter = new Dictionary<string, int>();
Dictionary<string, int> repeatCounter = [];
var appsIncluded = project.Applications.Where(x => x.IsIncluded);
IEnumerable<Application> appsIncluded = project.Applications.Where(x => x.IsIncluded);
foreach (Application app in appsIncluded)
{
if (repeatCounter.TryGetValue(app.AppPath + app.AppTitle, out int value))
string appIdentifier = app.AppPath + app.PwaAppId;
if (repeatCounter.TryGetValue(appIdentifier, out int value))
{
repeatCounter[app.AppPath + app.AppTitle] = ++value;
repeatCounter[appIdentifier] = ++value;
}
else
{
repeatCounter.Add(app.AppPath + app.AppTitle, 1);
repeatCounter.Add(appIdentifier, 1);
}
app.RepeatIndex = repeatCounter[app.AppPath + app.AppTitle];
app.RepeatIndex = repeatCounter[appIdentifier];
}
foreach (Application app in project.Applications.Where(x => !x.IsIncluded))
@ -113,7 +114,7 @@ namespace WorkspacesEditor.Utils
}
}
Bitmap previewBitmap = new Bitmap(Scaled(bounds.Width + (verticalGaps.Count * gapWidth)), Scaled((bounds.Height * 1.2) + (horizontalGaps.Count * gapHeight)));
Bitmap previewBitmap = new(Scaled(bounds.Width + (verticalGaps.Count * gapWidth)), Scaled((bounds.Height * 1.2) + (horizontalGaps.Count * gapHeight)));
double desiredIconSize = Scaled(Math.Min(bounds.Width, bounds.Height)) * 0.25;
using (Graphics g = Graphics.FromImage(previewBitmap))
{
@ -131,7 +132,7 @@ namespace WorkspacesEditor.Utils
g.FillRectangle(monitorBrush, new Rectangle(TransformX(monitor.MonitorDpiAwareBounds.Left), TransformY(monitor.MonitorDpiAwareBounds.Top), Scaled(monitor.MonitorDpiAwareBounds.Width), Scaled(monitor.MonitorDpiAwareBounds.Height)));
}
var appsToDraw = appsIncluded.Where(x => !x.Minimized);
IEnumerable<Application> appsToDraw = appsIncluded.Where(x => !x.Minimized);
// draw the highlighted app at the end to have its icon in the foreground for the case there are overlapping icons
foreach (Application app in appsToDraw.Where(x => !x.IsHighlighted))
@ -147,24 +148,22 @@ namespace WorkspacesEditor.Utils
}
// draw the minimized windows
Rectangle rectMinimized = new Rectangle(0, Scaled((bounds.Height * 1.02) + (horizontalGaps.Count * gapHeight)), Scaled(bounds.Width + (verticalGaps.Count * gapWidth)), Scaled(bounds.Height * 0.18));
Rectangle rectMinimized = new(0, Scaled((bounds.Height * 1.02) + (horizontalGaps.Count * gapHeight)), Scaled(bounds.Width + (verticalGaps.Count * gapWidth)), Scaled(bounds.Height * 0.18));
DrawWindow(g, brush, brushForHighlight, rectMinimized, appsIncluded.Where(x => x.Minimized), currentTheme);
}
using (var memory = new MemoryStream())
{
previewBitmap.Save(memory, ImageFormat.Png);
memory.Position = 0;
using MemoryStream memory = new();
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();
BitmapImage bitmapImage = new();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memory;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.Freeze();
return bitmapImage;
}
return bitmapImage;
}
public static void DrawWindow(Graphics graphics, Brush brush, Rectangle bounds, Application app, double desiredIconSize, Theme currentTheme)
@ -194,7 +193,7 @@ namespace WorkspacesEditor.Utils
}
double iconSize = Math.Min(Math.Min(bounds.Width - 4, bounds.Height - 4), desiredIconSize);
Rectangle iconBounds = new Rectangle((int)(bounds.Left + (bounds.Width / 2) - (iconSize / 2)), (int)(bounds.Top + (bounds.Height / 2) - (iconSize / 2)), (int)iconSize, (int)iconSize);
Rectangle iconBounds = new((int)(bounds.Left + (bounds.Width / 2) - (iconSize / 2)), (int)(bounds.Top + (bounds.Height / 2) - (iconSize / 2)), (int)iconSize, (int)iconSize);
try
{
@ -203,13 +202,13 @@ namespace WorkspacesEditor.Utils
{
string indexString = app.RepeatIndex.ToString(CultureInfo.InvariantCulture);
int indexSize = (int)(iconBounds.Width * 0.5);
Rectangle indexBounds = new Rectangle(iconBounds.Right - indexSize, iconBounds.Bottom - indexSize, indexSize, indexSize);
Rectangle indexBounds = new(iconBounds.Right - indexSize, iconBounds.Bottom - indexSize, indexSize, indexSize);
var textSize = graphics.MeasureString(indexString, font);
var state = graphics.Save();
SizeF textSize = graphics.MeasureString(indexString, Font);
GraphicsState 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.DrawString(indexString, Font, Brushes.Black, PointF.Empty);
graphics.Restore(state);
}
}
@ -255,7 +254,7 @@ namespace WorkspacesEditor.Utils
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);
Rectangle iconBounds = new((int)(bounds.Left + (bounds.Width / 2) - (iconSize * ((appsCount / 2) - iconCounter))), (int)(bounds.Top + (bounds.Height / 2) - (iconSize / 2)), (int)iconSize, (int)iconSize);
try
{
@ -264,13 +263,13 @@ namespace WorkspacesEditor.Utils
{
string indexString = app.RepeatIndex.ToString(CultureInfo.InvariantCulture);
int indexSize = (int)(iconBounds.Width * 0.5);
Rectangle indexBounds = new Rectangle(iconBounds.Right - indexSize, iconBounds.Bottom - indexSize, indexSize, indexSize);
Rectangle indexBounds = new(iconBounds.Right - indexSize, iconBounds.Bottom - indexSize, indexSize, indexSize);
var textSize = graphics.MeasureString(indexString, font);
var state = graphics.Save();
SizeF textSize = graphics.MeasureString(indexString, Font);
GraphicsState 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.DrawString(indexString, Font, Brushes.Black, PointF.Empty);
graphics.Restore(state);
}
}
@ -289,14 +288,14 @@ namespace WorkspacesEditor.Utils
return null;
}
Bitmap previewBitmap = new Bitmap(32 * appsCount, 24);
Bitmap previewBitmap = new(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 project.Applications)
foreach (Application app in project.Applications)
{
try
{
@ -311,20 +310,18 @@ namespace WorkspacesEditor.Utils
}
}
using (var memory = new MemoryStream())
{
previewBitmap.Save(memory, ImageFormat.Png);
memory.Position = 0;
using MemoryStream memory = new();
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();
BitmapImage bitmapImage = new();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memory;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.Freeze();
return bitmapImage;
}
return bitmapImage;
}
private static GraphicsPath RoundedRect(Rectangle bounds)
@ -333,9 +330,9 @@ namespace WorkspacesEditor.Utils
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();
Size size = new(diameter, diameter);
Rectangle arc = new(bounds.Location, size);
GraphicsPath path = new();
if (radius == 0)
{

Просмотреть файл

@ -26,18 +26,16 @@ namespace WorkspacesEditor.Utils
{
if (_fileSystem.File.Exists(fileName))
{
var attempts = 0;
int attempts = 0;
while (attempts < 10)
{
try
{
using (FileSystemStream inputStream = _fileSystem.File.Open(fileName, FileMode.Open))
using (StreamReader reader = new StreamReader(inputStream))
{
string data = reader.ReadToEnd();
inputStream.Close();
return data;
}
using FileSystemStream inputStream = _fileSystem.File.Open(fileName, FileMode.Open);
using StreamReader reader = new(inputStream);
string data = reader.ReadToEnd();
inputStream.Close();
return data;
}
catch (Exception)
{

Просмотреть файл

@ -26,7 +26,7 @@ namespace WorkspacesEditor.Utils
private Screen[] GetDpiUnawareScreenBounds()
{
Thread dpiUnawareThread = new Thread(new ThreadStart(SaveDpiUnawareScreens));
Thread dpiUnawareThread = new(new ThreadStart(SaveDpiUnawareScreens));
dpiUnawareThread.Start();
dpiUnawareThread.Join();
@ -35,15 +35,15 @@ namespace WorkspacesEditor.Utils
public static Screen[] GetDpiUnawareScreens()
{
MonitorHelper monitorHelper = new MonitorHelper();
MonitorHelper monitorHelper = new();
return monitorHelper.GetDpiUnawareScreenBounds();
}
internal static double GetScreenDpiFromScreen(Screen screen)
{
var point = new System.Drawing.Point(screen.Bounds.Left + 1, screen.Bounds.Top + 1);
var monitor = NativeMethods.MonitorFromPoint(point, NativeMethods._MONITOR_DEFAULTTONEAREST);
NativeMethods.GetDpiForMonitor(monitor, NativeMethods.DpiType.EFFECTIVE, out uint dpiX, out uint dpiY);
System.Drawing.Point point = new(screen.Bounds.Left + 1, screen.Bounds.Top + 1);
nint monitor = NativeMethods.MonitorFromPoint(point, NativeMethods._MONITOR_DEFAULTTONEAREST);
_ = NativeMethods.GetDpiForMonitor(monitor, NativeMethods.DpiType.EFFECTIVE, out uint dpiX, out _);
return dpiX / 96.0;
}
}

Просмотреть файл

@ -4,19 +4,12 @@
namespace WorkspacesEditor.Utils
{
public struct ParsingResult
public readonly struct ParsingResult(bool result, string message = "", string data = "")
{
public bool Result { get; }
public bool Result { get; } = result;
public string Message { get; }
public string Message { get; } = message;
public string MalformedData { get; }
public ParsingResult(bool result, string message = "", string data = "")
{
Result = result;
Message = message;
MalformedData = data;
}
public string MalformedData { get; } = data;
}
}

Просмотреть файл

@ -9,13 +9,13 @@ namespace WorkspacesEditor.Utils
public class Settings
{
private const string WorkspacesModuleName = "Workspaces";
private static SettingsUtils _settingsUtils = new SettingsUtils();
private static readonly SettingsUtils _settingsUtils = new();
public static WorkspacesSettings ReadSettings()
{
if (!_settingsUtils.SettingsExists(WorkspacesModuleName))
{
var defaultWorkspacesSettings = new WorkspacesSettings();
WorkspacesSettings defaultWorkspacesSettings = new();
defaultWorkspacesSettings.Save(_settingsUtils);
return defaultWorkspacesSettings;
}

Просмотреть файл

@ -11,12 +11,9 @@ namespace WorkspacesEditor.Utils
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();
return str.Length == 1
? str
: string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "-" + x.ToString() : x.ToString())).ToLowerInvariant();
}
}
}

Просмотреть файл

@ -24,7 +24,7 @@ namespace WorkspacesEditor.Utils
{
try
{
WorkspacesData parser = new WorkspacesData();
WorkspacesData parser = new();
if (!File.Exists(parser.File))
{
Logger.LogWarning($"Workspaces storage file not found: {parser.File}");
@ -56,14 +56,14 @@ namespace WorkspacesEditor.Utils
{
try
{
ProjectData parser = new ProjectData();
ProjectData parser = new();
if (!File.Exists(TempProjectData.File))
{
Logger.LogWarning($"ParseProject method. Workspaces storage file not found: {TempProjectData.File}");
return null;
}
Project project = new Project(parser.Read(TempProjectData.File));
Project project = new(parser.Read(TempProjectData.File));
return project;
}
catch (Exception e)
@ -75,13 +75,13 @@ namespace WorkspacesEditor.Utils
public void SerializeWorkspaces(List<Project> workspaces, bool useTempFile = false)
{
WorkspacesData serializer = new WorkspacesData();
WorkspacesData.WorkspacesListWrapper workspacesWrapper = new WorkspacesData.WorkspacesListWrapper { };
workspacesWrapper.Workspaces = new List<ProjectData.ProjectWrapper>();
WorkspacesData serializer = new();
WorkspacesData.WorkspacesListWrapper workspacesWrapper = new() { };
workspacesWrapper.Workspaces = [];
foreach (Project project in workspaces)
{
ProjectData.ProjectWrapper wrapper = new ProjectData.ProjectWrapper
ProjectData.ProjectWrapper wrapper = new()
{
Id = project.Id,
Name = project.Name,
@ -89,11 +89,11 @@ namespace WorkspacesEditor.Utils
IsShortcutNeeded = project.IsShortcutNeeded,
MoveExistingWindows = project.MoveExistingWindows,
LastLaunchedTime = project.LastLaunchedTime,
Applications = new List<ProjectData.ApplicationWrapper> { },
MonitorConfiguration = new List<ProjectData.MonitorConfigurationWrapper> { },
Applications = [],
MonitorConfiguration = [],
};
foreach (var app in project.Applications.Where(x => x.IsIncluded))
foreach (Application app in project.Applications.Where(x => x.IsIncluded))
{
wrapper.Applications.Add(new ProjectData.ApplicationWrapper
{
@ -120,7 +120,7 @@ namespace WorkspacesEditor.Utils
});
}
foreach (var monitor in project.Monitors)
foreach (MonitorSetup monitor in project.Monitors)
{
wrapper.MonitorConfiguration.Add(new ProjectData.MonitorConfigurationWrapper
{
@ -150,7 +150,7 @@ namespace WorkspacesEditor.Utils
try
{
IOUtils ioUtils = new IOUtils();
IOUtils ioUtils = new();
ioUtils.WriteFile(useTempFile ? TempProjectData.File : serializer.File, serializer.Serialize(workspacesWrapper));
}
catch (Exception e)
@ -162,7 +162,7 @@ namespace WorkspacesEditor.Utils
private bool AddWorkspaces(MainViewModel mainViewModel, WorkspacesData.WorkspacesListWrapper workspaces)
{
foreach (var project in workspaces.Workspaces)
foreach (ProjectData.ProjectWrapper project in workspaces.Workspaces)
{
mainViewModel.Workspaces.Add(new Project(project));
}
@ -173,13 +173,13 @@ namespace WorkspacesEditor.Utils
private bool SetWorkspaces(MainViewModel mainViewModel, WorkspacesData.WorkspacesListWrapper workspaces)
{
mainViewModel.Workspaces = new System.Collections.ObjectModel.ObservableCollection<Project> { };
mainViewModel.Workspaces = [];
return AddWorkspaces(mainViewModel, workspaces);
}
internal void SerializeTempProject(Project project)
{
SerializeWorkspaces(new List<Project>() { project }, true);
SerializeWorkspaces([project], true);
}
}
}

Просмотреть файл

@ -156,24 +156,33 @@
Content="{x:Static props:Resources.LaunchAsAdmin}"
IsChecked="{Binding IsElevated, Mode=TwoWay}"
IsEnabled="{Binding CanLaunchElevated, Mode=OneWay}" />
<CheckBox
MinWidth="10"
Margin="15,0,0,0"
Checked="MaximizedChecked"
Content="{x:Static props:Resources.Maximized}"
IsChecked="{Binding Maximized, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<CheckBox
MinWidth="10"
Margin="15,0,0,0"
Checked="MinimizedChecked"
Content="{x:Static props:Resources.Minimized}"
IsChecked="{Binding Minimized, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
<StackPanel
Grid.Row="2"
Margin="100,5,0,0"
Orientation="Horizontal">
<TextBlock
VerticalAlignment="Center"
FontSize="14"
FontWeight="Normal"
IsEnabled="{Binding EditPositionEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource TextBlockEnabledStyle}"
Text="{x:Static props:Resources.WindowPosition}" />
<ComboBox
Margin="15,0,0,0"
VerticalAlignment="Center"
Background="{DynamicResource SecondaryBackgroundBrush}"
BorderBrush="{DynamicResource PrimaryBorderBrush}"
BorderThickness="2"
FontSize="14"
FontWeight="Normal"
SelectedIndex="{Binding PositionComboboxIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ComboBoxItem Content="{x:Static props:Resources.Custom}" />
<ComboBoxItem Content="{x:Static props:Resources.Maximized}" />
<ComboBoxItem Content="{x:Static props:Resources.Minimized}" />
</ComboBox>
<TextBlock
Margin="15,0,0,0"
VerticalAlignment="Center"
FontSize="14"
FontWeight="Normal"
@ -267,14 +276,12 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<Button
Margin="0,20,0,20"
Margin="0,10,0,20"
VerticalAlignment="Center"
Background="Transparent"
Click="CancelButtonClicked">
@ -300,27 +307,8 @@
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{Binding EditorWindowTitle}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Vertical">
<TextBlock
FontSize="14"
FontWeight="Normal"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{x:Static props:Resources.WorkspaceName}" />
<TextBox
x:Name="EditNameTextBox"
Width="320"
Margin="0,6,0,6"
HorizontalAlignment="Left"
Background="{DynamicResource SecondaryBackgroundBrush}"
BorderBrush="{DynamicResource PrimaryBorderBrush}"
BorderThickness="2"
GotFocus="EditNameTextBox_GotFocus"
KeyDown="EditNameTextBoxKeyDown"
Text="{Binding Name, Mode=TwoWay}"
TextChanged="EditNameTextBox_TextChanged" />
</StackPanel>
<Border
Grid.Row="2"
Grid.Row="1"
HorizontalAlignment="Stretch"
Background="{DynamicResource MonitorViewBackgroundBrush}"
CornerRadius="5">
@ -361,8 +349,47 @@
DockPanel.Dock="Right" />
</DockPanel>
</Border>
<DockPanel Grid.Row="2" HorizontalAlignment="Stretch">
<StackPanel Orientation="Vertical">
<TextBlock
Margin="0,10,0,0"
FontSize="14"
FontWeight="Normal"
Foreground="{DynamicResource PrimaryForegroundBrush}"
Text="{x:Static props:Resources.WorkspaceName}" />
<TextBox
x:Name="EditNameTextBox"
Width="300"
Margin="0,6,0,6"
HorizontalAlignment="Left"
Background="{DynamicResource SecondaryBackgroundBrush}"
BorderBrush="{DynamicResource PrimaryBorderBrush}"
BorderThickness="2"
GotFocus="EditNameTextBox_GotFocus"
KeyDown="EditNameTextBoxKeyDown"
Text="{Binding Name, Mode=TwoWay}"
TextChanged="EditNameTextBox_TextChanged" />
</StackPanel>
<StackPanel
HorizontalAlignment="Right"
DockPanel.Dock="Right"
Orientation="Horizontal">
<CheckBox
Margin="20,0,0,0"
VerticalAlignment="Bottom"
Content="{x:Static props:Resources.CreateShortcut}"
FontSize="14"
IsChecked="{Binding IsShortcutNeeded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<CheckBox
Margin="20,0,0,0"
VerticalAlignment="Bottom"
Content="{x:Static props:Resources.MoveIfExist}"
FontSize="14"
IsChecked="{Binding MoveExistingWindows, Mode=TwoWay}" />
</StackPanel>
</DockPanel>
<ScrollViewer
Grid.Row="4"
Grid.Row="3"
Margin="0,10,0,0"
PreviewMouseWheel="ScrollViewer_PreviewMouseWheel"
VerticalScrollBarVisibility="Auto">
@ -377,52 +404,31 @@
</StackPanel>
</ScrollViewer>
<StackPanel
Grid.Row="5"
Margin="0,5,0,0"
Orientation="Horizontal"
Visibility="Collapsed">
<CheckBox
Margin="0,0,0,0"
VerticalAlignment="Center"
Content="{x:Static props:Resources.MoveIfExist}"
FontSize="14"
FontWeight="Normal"
Foreground="{DynamicResource PrimaryForegroundBrush}"
IsChecked="{Binding MoveExistingWindows, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
Grid.Row="4"
Margin="40,20,0,0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
x:Name="CancelButton"
Height="36"
Margin="20,0,0,0"
Padding="24,0,24,0"
AutomationProperties.Name="{x:Static props:Resources.Cancel}"
Background="{DynamicResource SecondaryBackgroundBrush}"
BorderBrush="{DynamicResource PrimaryBorderBrush}"
BorderThickness="2"
Click="CancelButtonClicked"
Content="{x:Static props:Resources.Cancel}" />
<Button
x:Name="SaveButton"
Height="36"
Margin="20,0,0,0"
Padding="24,0,24,0"
AutomationProperties.Name="{x:Static props:Resources.Save_Workspace}"
Click="SaveButtonClicked"
Content="{x:Static props:Resources.Save_Workspace}"
IsEnabled="{Binding CanBeSaved, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
<DockPanel Grid.Row="6" Margin="0,20,0,20">
<CheckBox
Content="{x:Static props:Resources.CreateShortcut}"
DockPanel.Dock="Left"
FontSize="14"
IsChecked="{Binding IsShortcutNeeded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<StackPanel
Margin="40,0,0,0"
HorizontalAlignment="Right"
DockPanel.Dock="Right"
Orientation="Horizontal">
<Button
x:Name="CancelButton"
Height="36"
Margin="20,0,0,0"
Padding="24,0,24,0"
AutomationProperties.Name="{x:Static props:Resources.Cancel}"
Background="{DynamicResource SecondaryBackgroundBrush}"
BorderBrush="{DynamicResource PrimaryBorderBrush}"
BorderThickness="2"
Click="CancelButtonClicked"
Content="{x:Static props:Resources.Cancel}" />
<Button
x:Name="SaveButton"
Height="36"
Margin="20,0,0,0"
Padding="24,0,24,0"
AutomationProperties.Name="{x:Static props:Resources.Save_Workspace}"
Click="SaveButtonClicked"
Content="{x:Static props:Resources.Save_Workspace}"
IsEnabled="{Binding CanBeSaved, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
</DockPanel>
</Grid>
</Page>

Просмотреть файл

@ -185,20 +185,6 @@ namespace WorkspacesEditor
application.CommandLineTextChanged(textBox.Text);
}
private void MaximizedChecked(object sender, RoutedEventArgs e)
{
CheckBox checkBox = sender as CheckBox;
Models.Application application = checkBox.DataContext as Models.Application;
application.MaximizedChecked();
}
private void MinimizedChecked(object sender, RoutedEventArgs e)
{
CheckBox checkBox = sender as CheckBox;
Models.Application application = checkBox.DataContext as Models.Application;
application.MinimizedChecked();
}
private void LaunchEditButtonClicked(object sender, RoutedEventArgs e)
{
Button button = sender as Button;

Просмотреть файл

@ -101,7 +101,7 @@ void Launcher::Launch() // Launching thread
const long ms = 100;
// Launch apps
for (auto appState = m_launchingStatus.GetNext(LaunchingState::Waiting); appState.has_value(); appState = m_launchingStatus.GetNext(LaunchingState::Waiting))
for (auto appState = m_launchingStatus.GetNext(LaunchingState::Waiting);appState.has_value();appState = m_launchingStatus.GetNext(LaunchingState::Waiting))
{
auto app = appState.value().application;
@ -157,7 +157,7 @@ void Launcher::Launch() // Launching thread
{
std::lock_guard lock(m_uiHelperMutex);
m_uiHelper->UpdateLaunchStatus(m_launchingStatus.Get());
}
};
}
}

Просмотреть файл

@ -31,6 +31,9 @@ namespace Utils
constexpr const wchar_t* PowerToysSettings = L"PowerToys.Settings.exe";
constexpr const wchar_t* ApplicationFrameHost = L"APPLICATIONFRAMEHOST.EXE";
constexpr const wchar_t* Exe = L".EXE";
constexpr const wchar_t* EdgeFilename = L"msedge.exe";
constexpr const wchar_t* ChromeFilename = L"chrome.exe";
}
AppList IterateAppsFolder()
@ -214,7 +217,7 @@ namespace Utils
std::wstring appPathUpper(appPath);
std::transform(appPathUpper.begin(), appPathUpper.end(), appPathUpper.begin(), towupper);
// filter out ApplicationFrameHost.exe
// filter out ApplicationFrameHost.exe
if (appPathUpper.ends_with(NonLocalizable::ApplicationFrameHost))
{
return std::nullopt;
@ -325,7 +328,7 @@ namespace Utils
}
}
}
return AppData{
.name = std::filesystem::path(appPath).stem(),
.installPath = appPath
@ -335,7 +338,7 @@ namespace Utils
std::optional<AppData> GetApp(HWND window, const AppList& apps)
{
std::wstring processPath = get_process_path(window);
DWORD pid{};
GetWindowThreadProcessId(window, &pid);
@ -378,5 +381,15 @@ namespace Utils
return updated;
}
bool AppData::IsEdge() const
{
return installPath.ends_with(NonLocalizable::EdgeFilename);
}
bool AppData::IsChrome() const
{
return installPath.ends_with(NonLocalizable::ChromeFilename);
}
}
}

Просмотреть файл

@ -12,7 +12,11 @@ namespace Utils
std::wstring installPath;
std::wstring packageFullName;
std::wstring appUserModelId;
std::wstring pwaAppId;
bool canLaunchElevated = false;
bool IsEdge() const;
bool IsChrome() const;
};
using AppList = std::vector<AppData>;

Просмотреть файл

@ -1,5 +1,6 @@
#include "pch.h"
#include "PwaHelper.h"
#include "AppUtils.h"
#include <ShlObj.h>
#include <tlhelp32.h>
#include <winternl.h>
@ -11,7 +12,7 @@
#include <wil\com.h>
#pragma comment(lib, "ntdll.lib")
namespace SnapshotUtils
namespace Utils
{
namespace NonLocalizable
{
@ -22,6 +23,7 @@ namespace SnapshotUtils
const std::wstring ChromeDirPrefix = L"_crx_";
const std::wstring EdgeDirPrefix = L"_crx__";
}
// {c8900b66-a973-584b-8cae-355b7f55341b}
DEFINE_GUID(CLSID_StartMenuCacheAndAppResolver, 0x660b90c8, 0x73a9, 0x4b58, 0x8c, 0xae, 0x35, 0x5b, 0x7f, 0x55, 0x34, 0x1b);
@ -48,9 +50,10 @@ namespace SnapshotUtils
virtual HRESULT STDMETHODCALLTYPE GetAppIDForProcess(DWORD dwProcessId, WCHAR** pszAppId, void* pUnknown1, void* pUnknown2, void* pUnknown3) = 0;
};
BOOL GetAppId_7(HWND hWnd, std::wstring* result)
std::optional<std::wstring> PwaHelper::GetAppId_7(HWND hWnd) const
{
HRESULT hr;
std::optional<std::wstring> result = std::nullopt;
wil::com_ptr<IAppResolver_7> appResolver;
hr = CoCreateInstance(CLSID_StartMenuCacheAndAppResolver, NULL, CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, IID_IAppResolver_7, reinterpret_cast<void**>(appResolver.put()));
@ -60,19 +63,19 @@ namespace SnapshotUtils
hr = appResolver->GetAppIDForWindow(hWnd, &pszAppId, NULL, NULL, NULL);
if (SUCCEEDED(hr))
{
*result = std::wstring(pszAppId.get());
result = std::wstring(pszAppId.get());
}
appResolver->Release();
}
return SUCCEEDED(hr);
return result;
}
BOOL GetAppId_8(HWND hWnd, std::wstring* result)
std::optional<std::wstring> PwaHelper::GetAppId_8(HWND hWnd) const
{
HRESULT hr;
*result = L"";
std::optional<std::wstring> result = std::nullopt;
wil::com_ptr<IAppResolver_8> appResolver;
hr = CoCreateInstance(CLSID_StartMenuCacheAndAppResolver, NULL, CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, IID_IAppResolver_8, reinterpret_cast<void**>(appResolver.put()));
@ -82,29 +85,29 @@ namespace SnapshotUtils
hr = appResolver->GetAppIDForWindow(hWnd, &pszAppId, NULL, NULL, NULL);
if (SUCCEEDED(hr))
{
*result = std::wstring(pszAppId.get());
result = std::wstring(pszAppId.get());
}
appResolver->Release();
}
return SUCCEEDED(hr);
return result;
}
BOOL PwaHelper::GetAppId(HWND hWnd, std::wstring* result)
std::wstring PwaHelper::GetAppId(HWND hWnd) const
{
HRESULT hr = GetAppId_8(hWnd, result);
if (!SUCCEEDED(hr))
std::optional<std::wstring> result = GetAppId_8(hWnd);
if (result == std::nullopt)
{
hr = GetAppId_7(hWnd, result);
result = GetAppId_7(hWnd);
}
return SUCCEEDED(hr);
return result.has_value() ? result.value() : L"";
}
BOOL GetProcessId_7(DWORD dwProcessId, std::wstring* result)
std::optional<std::wstring> PwaHelper::GetProcessId_7(DWORD dwProcessId) const
{
HRESULT hr;
*result = L"";
std::optional<std::wstring> result = std::nullopt;
wil::com_ptr<IAppResolver_7> appResolver;
hr = CoCreateInstance(CLSID_StartMenuCacheAndAppResolver, NULL, CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, IID_IAppResolver_7, reinterpret_cast<void**>(appResolver.put()));
@ -114,19 +117,19 @@ namespace SnapshotUtils
hr = appResolver->GetAppIDForProcess(dwProcessId, &pszAppId, NULL, NULL, NULL);
if (SUCCEEDED(hr))
{
*result = std::wstring(pszAppId.get());
result = std::wstring(pszAppId.get());
}
appResolver->Release();
}
return SUCCEEDED(hr);
return result;
}
BOOL GetProcessId_8(DWORD dwProcessId, std::wstring* result)
std::optional<std::wstring> PwaHelper::GetProcessId_8(DWORD dwProcessId) const
{
HRESULT hr;
*result = L"";
std::optional<std::wstring> result = std::nullopt;
wil::com_ptr<IAppResolver_8> appResolver;
hr = CoCreateInstance(CLSID_StartMenuCacheAndAppResolver, NULL, CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, IID_IAppResolver_8, reinterpret_cast<void**>(appResolver.put()));
@ -136,23 +139,23 @@ namespace SnapshotUtils
hr = appResolver->GetAppIDForProcess(dwProcessId, &pszAppId, NULL, NULL, NULL);
if (SUCCEEDED(hr))
{
*result = std::wstring(pszAppId.get());
result = std::wstring(pszAppId.get());
}
appResolver->Release();
}
return SUCCEEDED(hr);
return result;
}
BOOL GetProcessId(DWORD dwProcessId, std::wstring* result)
std::wstring PwaHelper::GetProcessId(DWORD dwProcessId) const
{
HRESULT hr = GetProcessId_8(dwProcessId, result);
if (!SUCCEEDED(hr))
std::optional<std::wstring> result = GetProcessId_8(dwProcessId);
if (result == std::nullopt)
{
hr = GetProcessId_7(dwProcessId, result);
result = GetProcessId_7(dwProcessId);
}
return SUCCEEDED(hr);
return result.has_value() ? result.value() : L"";
}
std::wstring GetProcCommandLine(DWORD pid)
@ -244,7 +247,7 @@ namespace SnapshotUtils
void PwaHelper::InitAumidToAppId()
{
if (pwaAumidToAppId.size() > 0)
if (m_pwaAumidToAppId.size() > 0)
{
return;
}
@ -254,7 +257,7 @@ namespace SnapshotUtils
for (const auto subProcessID : pwaHelperProcessIds)
{
std::wstring aumidID;
GetProcessId(subProcessID, &aumidID);
aumidID = GetProcessId(subProcessID);
std::wstring commandLineArg = GetProcCommandLine(subProcessID);
auto appIdIndexStart = commandLineArg.find(NonLocalizable::EdgeAppIdIdentifier);
if (appIdIndexStart != std::wstring::npos)
@ -267,7 +270,7 @@ namespace SnapshotUtils
}
}
std::wstring appId{ commandLineArg };
pwaAumidToAppId.insert(std::map<std::wstring, std::wstring>::value_type(aumidID, appId));
m_pwaAumidToAppId.insert(std::map<std::wstring, std::wstring>::value_type(aumidID, appId));
Logger::info(L"Found an edge Pwa helper process with AumidID {} and PwaAppId {}", aumidID, appId);
PWSTR path = NULL;
@ -293,7 +296,7 @@ namespace SnapshotUtils
const std::filesystem::path filenameString = filename.path().filename();
if (filenameString.extension().wstring() == L".ico")
{
pwaAppIdsToAppNames.insert(std::map<std::wstring, std::wstring>::value_type(appId, filenameString.stem().wstring()));
m_pwaAppIdsToAppNames.insert(std::map<std::wstring, std::wstring>::value_type(appId, filenameString.stem().wstring()));
Logger::info(L"Storing an edge Pwa app name {} for PwaAppId {}", filenameString.stem().wstring(), appId);
}
}
@ -307,41 +310,39 @@ namespace SnapshotUtils
}
}
BOOL PwaHelper::GetPwaAppId(std::wstring windowAumid, std::wstring* result)
std::optional<std::wstring> PwaHelper::GetPwaAppId(const std::wstring& windowAumid) const
{
const auto pwaIndex = pwaAumidToAppId.find(windowAumid);
if (pwaIndex != pwaAumidToAppId.end())
const auto pwaIndex = m_pwaAumidToAppId.find(windowAumid);
if (pwaIndex != m_pwaAumidToAppId.end())
{
*result = pwaIndex->second;
return true;
return pwaIndex->second;
}
return false;
return std::nullopt;
;
}
BOOL PwaHelper::SearchPwaName(std::wstring pwaAppId, std::wstring windowAumid, std::wstring* pwaName)
std::wstring PwaHelper::SearchPwaName(const std::wstring& pwaAppId, const std::wstring& windowAumid) const
{
const auto index = pwaAppIdsToAppNames.find(pwaAppId);
if (index != pwaAppIdsToAppNames.end())
const auto index = m_pwaAppIdsToAppNames.find(pwaAppId);
if (index != m_pwaAppIdsToAppNames.end())
{
*pwaName = index->second;
return true;
return index->second;
}
std::wstring nameFromAumid{ windowAumid };
const std::size_t delimiterPos = nameFromAumid.find(L"-");
if (delimiterPos != std::string::npos)
{
nameFromAumid = nameFromAumid.substr(0, delimiterPos);
return nameFromAumid.substr(0, delimiterPos);
}
*pwaName = nameFromAumid;
return false;
return nameFromAumid;
}
void PwaHelper::InitChromeAppIds()
{
if (chromeAppIds.size() > 0)
if (m_chromeAppIds.size() > 0)
{
return;
}
@ -360,7 +361,7 @@ namespace SnapshotUtils
if (directoryName.wstring().find(NonLocalizable::ChromeDirPrefix) == 0)
{
const std::wstring appId = directoryName.wstring().substr(NonLocalizable::ChromeDirPrefix.size());
chromeAppIds.push_back(appId);
m_chromeAppIds.push_back(appId);
for (const auto& filename : std::filesystem::directory_iterator(directory))
{
if (!filename.is_directory())
@ -368,7 +369,7 @@ namespace SnapshotUtils
const std::filesystem::path filenameString = filename.path().filename();
if (filenameString.extension().wstring() == L".ico")
{
pwaAppIdsToAppNames.insert(std::map<std::wstring, std::wstring>::value_type(appId, filenameString.stem().wstring()));
m_pwaAppIdsToAppNames.insert(std::map<std::wstring, std::wstring>::value_type(appId, filenameString.stem().wstring()));
Logger::info(L"Found an installed chrome Pwa app {} with PwaAppId {}", filenameString.stem().wstring(), appId);
}
}
@ -380,30 +381,73 @@ namespace SnapshotUtils
}
}
BOOL PwaHelper::SearchPwaAppId(std::wstring windowAumid, std::wstring* pwaAppId)
std::optional<std::wstring> PwaHelper::SearchPwaAppId(const std::wstring& windowAumid) const
{
const auto appIdIndexStart = windowAumid.find(NonLocalizable::ChromeAppIdIdentifier);
if (appIdIndexStart != std::wstring::npos)
{
windowAumid = windowAumid.substr(appIdIndexStart + NonLocalizable::ChromeAppIdIdentifier.size());
const auto appIdIndexEnd = windowAumid.find(L" ");
std::wstring windowAumidSub = windowAumid.substr(appIdIndexStart + NonLocalizable::ChromeAppIdIdentifier.size());
const auto appIdIndexEnd = windowAumidSub.find(L" ");
if (appIdIndexEnd != std::wstring::npos)
{
windowAumid = windowAumid.substr(0, appIdIndexEnd);
windowAumidSub = windowAumidSub.substr(0, appIdIndexEnd);
}
const std::wstring windowAumidBegin = windowAumid.substr(0, 10);
for (const auto chromeAppId : chromeAppIds)
const std::wstring windowAumidBegin = windowAumidSub.substr(0, 10);
for (const auto chromeAppId : m_chromeAppIds)
{
if (chromeAppId.find(windowAumidBegin) == 0)
{
*pwaAppId = chromeAppId;
return true;
return chromeAppId;
}
}
}
*pwaAppId = L"";
return false;
return std::nullopt;
}
void PwaHelper::UpdatePwaApp(Utils::Apps::AppData* appData, HWND window)
{
std::optional<std::wstring> pwaAppId = std::nullopt;
std::wstring finalName = appData->name;
std::wstring pwaName = L"";
if (appData->IsEdge())
{
InitAumidToAppId();
std::wstring windowAumid = GetAppId(window);
Logger::info(L"Found an edge window with aumid {}", windowAumid);
pwaAppId = GetPwaAppId(windowAumid);
if (pwaAppId.has_value())
{
Logger::info(L"The found edge window is a PWA app with appId {}", pwaAppId.value());
pwaName = SearchPwaName(pwaAppId.value(), windowAumid);
Logger::info(L"The found edge window is a PWA app with name {}", pwaName);
finalName = pwaName + L" (" + finalName + L")";
}
else
{
Logger::info(L"The found edge window does not contain a PWA app");
}
}
else if (appData->IsChrome())
{
InitChromeAppIds();
std::wstring windowAumid = GetAppId(window);
Logger::info(L"Found a chrome window with aumid {}", windowAumid);
pwaAppId = SearchPwaAppId(windowAumid);
if (pwaAppId.has_value())
{
pwaName = SearchPwaName(pwaAppId.value(), windowAumid);
finalName = pwaName + L" (" + finalName + L")";
}
}
appData->name = finalName;
appData->pwaAppId = pwaAppId.has_value() ? pwaAppId.value() : L"";
}
}

Просмотреть файл

@ -0,0 +1,29 @@
#pragma once
#include <WorkspacesLib/AppUtils.h>
namespace Utils
{
class PwaHelper
{
public:
void UpdatePwaApp(Apps::AppData* appData, HWND window);
private:
std::map<std::wstring, std::wstring> m_pwaAumidToAppId;
std::vector<std::wstring> m_chromeAppIds;
std::map<std::wstring, std::wstring> m_pwaAppIdsToAppNames;
void InitAumidToAppId();
void InitChromeAppIds();
std::optional<std::wstring> GetAppId_7(HWND hWnd) const;
std::optional<std::wstring> GetAppId_8(HWND hWnd) const;
std::wstring GetAppId(HWND hWnd) const;
std::optional<std::wstring> GetProcessId_7(DWORD dwProcessId) const;
std::optional<std::wstring> GetProcessId_8(DWORD dwProcessId) const;
std::wstring GetProcessId(DWORD dwProcessId) const;
std::optional<std::wstring> GetPwaAppId(const std::wstring& windowAumid) const;
std::wstring SearchPwaName(const std::wstring& pwaAppId, const std::wstring& windowAumid) const;
std::optional<std::wstring> SearchPwaAppId(const std::wstring& windowAumid) const;
};
}

Просмотреть файл

@ -1,6 +1,5 @@
#include "pch.h"
#include "WorkspacesData.h"
#include <common/SettingsAPI/settings_helpers.h>
#include <workspaces-common/GuidUtils.h>
@ -157,6 +156,7 @@ namespace WorkspacesData
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))
{

Просмотреть файл

@ -38,6 +38,7 @@
<ClInclude Include="LaunchingStateEnum.h" />
<ClInclude Include="LaunchingStatus.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="PwaHelper.h" />
<ClInclude Include="Result.h" />
<ClInclude Include="utils.h" />
<ClInclude Include="WorkspacesData.h" />
@ -51,6 +52,7 @@
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="PwaHelper.cpp" />
<ClCompile Include="two_way_pipe_message_ipc.cpp" />
<ClCompile Include="WorkspacesData.cpp" />
<ClCompile Include="trace.cpp" />
@ -70,6 +72,7 @@
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
@ -77,5 +80,6 @@
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

Просмотреть файл

@ -41,6 +41,9 @@
<ClInclude Include="LaunchingStatus.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="PwaHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
@ -67,6 +70,9 @@
<ClCompile Include="LaunchingStatus.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="PwaHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

Просмотреть файл

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

Просмотреть файл

@ -1,20 +0,0 @@
#pragma once
namespace SnapshotUtils
{
class PwaHelper
{
public:
void InitAumidToAppId();
BOOL GetAppId(HWND hWnd, std::wstring* result);
BOOL GetPwaAppId(std::wstring windowAumid, std::wstring* result);
BOOL SearchPwaName(std::wstring pwaAppId, std::wstring windowAumid, std::wstring* pwaName);
void InitChromeAppIds();
BOOL SearchPwaAppId(std::wstring windowAumid, std::wstring* pwaAppId);
private:
std::map<std::wstring, std::wstring> pwaAumidToAppId;
std::vector<std::wstring> chromeAppIds;
std::map<std::wstring, std::wstring> pwaAppIdsToAppNames;
};
}

Просмотреть файл

@ -9,7 +9,7 @@
#include <workspaces-common/WindowFilter.h>
#include <WorkspacesLib/AppUtils.h>
#include <PwaHelper.h>
#include <WorkspacesLib/PwaHelper.h>
#pragma comment(lib, "ntdll.lib")
@ -18,8 +18,6 @@ namespace SnapshotUtils
namespace NonLocalizable
{
const std::wstring ApplicationFrameHost = L"ApplicationFrameHost.exe";
const std::wstring EdgeFilename = L"msedge.exe";
const std::wstring ChromeFilename = L"chrome.exe";
}
bool IsProcessElevated(DWORD processID)
@ -40,19 +38,9 @@ namespace SnapshotUtils
return false;
}
bool IsEdge(Utils::Apps::AppData appData)
{
return appData.installPath.ends_with(NonLocalizable::EdgeFilename);
}
bool IsChrome(Utils::Apps::AppData appData)
{
return appData.installPath.ends_with(NonLocalizable::ChromeFilename);
}
std::vector<WorkspacesData::WorkspacesProject::Application> GetApps(const std::function<unsigned int(HWND)> getMonitorNumberFromWindowHandle, const std::function<WorkspacesData::WorkspacesProject::Monitor::MonitorRect(unsigned int)> getMonitorRect)
{
PwaHelper pwaHelper{};
Utils::PwaHelper pwaHelper{};
std::vector<WorkspacesData::WorkspacesProject::Application> apps{};
auto installedApps = Utils::Apps::GetAppsList();
@ -128,47 +116,7 @@ namespace SnapshotUtils
continue;
}
std::wstring pwaAppId = L"";
std::wstring finalName = data.value().name;
std::wstring pwaName = L"";
if (IsEdge(data.value()))
{
pwaHelper.InitAumidToAppId();
std::wstring windowAumid;
pwaHelper.GetAppId(window, &windowAumid);
Logger::info(L"Found an edge window with aumid {}", windowAumid);
if (pwaHelper.GetPwaAppId(windowAumid, &pwaAppId))
{
Logger::info(L"The found edge window is a PWA app with appId {}", pwaAppId);
if (pwaHelper.SearchPwaName(pwaAppId, windowAumid ,& pwaName))
{
Logger::info(L"The found edge window is a PWA app with name {}", finalName);
}
finalName = pwaName + L" (" + finalName + L")";
}
else
{
Logger::info(L"The found edge window does not contain a PWA app", pwaAppId);
}
}
else if (IsChrome(data.value()))
{
pwaHelper.InitChromeAppIds();
std::wstring windowAumid;
pwaHelper.GetAppId(window, &windowAumid);
Logger::info(L"Found a chrome window with aumid {}", windowAumid);
if (pwaHelper.SearchPwaAppId(windowAumid, &pwaAppId))
{
if (pwaHelper.SearchPwaName(pwaAppId, windowAumid, &pwaName))
{
finalName = pwaName + L" (" + finalName + L")";
}
}
}
pwaHelper.UpdatePwaApp(&data.value(), window);
bool isMinimized = WindowUtils::IsMinimized(window);
unsigned int monitorNumber = getMonitorNumberFromWindowHandle(window);
@ -184,12 +132,12 @@ namespace SnapshotUtils
}
WorkspacesData::WorkspacesProject::Application app{
.name = finalName,
.name = data.value().name,
.title = title,
.path = data.value().installPath,
.packageFullName = data.value().packageFullName,
.appUserModelId = data.value().appUserModelId,
.pwaAppId = pwaAppId,
.pwaAppId = data.value().pwaAppId,
.commandLineArgs = L"",
.isElevated = IsProcessElevated(pid),
.canLaunchElevated = data.value().canLaunchElevated,

Просмотреть файл

@ -130,12 +130,10 @@
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="PwaHelper.cpp" />
<ClCompile Include="SnapshotUtils.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="PwaHelper.h" />
<ClInclude Include="resource.base.h" />
<ClInclude Include="SnapshotUtils.h" />
</ItemGroup>

Просмотреть файл

@ -24,9 +24,6 @@
<ClInclude Include="resource.base.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="PwaHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
@ -38,9 +35,6 @@
<ClCompile Include="SnapshotUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="PwaHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

Просмотреть файл

@ -12,9 +12,18 @@
#include <workspaces-common/WindowUtils.h>
#include <WindowProperties/WorkspacesWindowPropertyUtils.h>
#include <WorkspacesLib/PwaHelper.h>
namespace FancyZones
namespace PlacementHelper
{
// When calculating the coordinates difference (== 'distance') between 2 windows, there are additional values added to the real distance
// if both windows are minimized, the 'distance' is 0, the minimal value, this is the best match, we prefer this 'pairing'
// if both are in normal state (non-minimized), we add 1 to the calculated 'distance', this is the 2nd best match
// if one window is minimized and the other is maximized, we add a high value (10.000) to the result as
// this case is the least desired match, we want this pairing (matching) only if there is no other possibility left
const int PlacementDistanceAdditionBothNormal = 1;
const int PlacementDistanceAdditionNormalAndMinimized = 10000;
inline void ScreenToWorkAreaCoords(HWND window, HMONITOR monitor, RECT& rect)
{
MONITORINFOEXW monitorInfo{ sizeof(MONITORINFOEXW) };
@ -52,14 +61,7 @@ namespace FancyZones
}
else
{
if ((placement.showCmd != SW_SHOWMINIMIZED) &&
(placement.showCmd != SW_MINIMIZE))
{
if (placement.showCmd == SW_SHOWMAXIMIZED)
placement.flags &= ~WPF_RESTORETOMAXIMIZED;
placement.showCmd = SW_RESTORE;
}
placement.showCmd = SW_RESTORE;
ScreenToWorkAreaCoords(window, monitor, rect);
placement.rcNormalPosition = rect;
@ -91,18 +93,191 @@ namespace FancyZones
return true;
}
int CalculateDistance(const WorkspacesData::WorkspacesProject::Application& app, HWND window)
{
WINDOWPLACEMENT placement{};
::GetWindowPlacement(window, &placement);
if (app.isMinimized && (placement.showCmd == SW_SHOWMINIMIZED))
{
// The most preferred case: both windows are minimized. The 'distance' between these 2 windows is 0, the lowest value
return 0;
}
int placementDiffPenalty = PlacementDistanceAdditionBothNormal;
if (app.isMinimized || (placement.showCmd == SW_SHOWMINIMIZED))
{
// The least preferred case: one window is minimized the other one isn't.
// We add a high number to the real distance, as we want this 2 windows be matched only if there is no other match
placementDiffPenalty = PlacementDistanceAdditionNormalAndMinimized;
}
RECT windowPosition;
GetWindowRect(window, &windowPosition);
DPIAware::InverseConvert(MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY), windowPosition);
return placementDiffPenalty + abs(app.position.x - windowPosition.left) + abs(app.position.y - windowPosition.top) + abs(app.position.x + app.position.width - windowPosition.right) + abs(app.position.y + app.position.height - windowPosition.bottom);
}
}
bool WindowArranger::TryMoveWindow(const WorkspacesData::WorkspacesProject::Application& app, HWND windowToMove)
{
Logger::info(L"The app {} is found at launch, moving it", app.name);
auto appState = m_launchingStatus.Get(app);
if (!appState.has_value())
{
Logger::info(L"The app {} is not found in the map of apps", app.name);
return false;
}
bool success = moveWindow(windowToMove, app);
if (success)
{
m_launchingStatus.Update(appState.value().application, windowToMove, LaunchingState::LaunchedAndMoved);
}
else
{
Logger::info(L"Failed to move the existing app {} ", app.name);
m_launchingStatus.Update(appState.value().application, windowToMove, LaunchingState::Failed);
}
auto updatedState = m_launchingStatus.Get(app);
if (updatedState.has_value())
{
m_ipcHelper.send(WorkspacesData::AppLaunchInfoJSON::ToJson(updatedState.value()).ToString().c_str());
}
return success;
}
std::optional<WindowWithDistance> WindowArranger::GetNearestWindow(const WorkspacesData::WorkspacesProject::Application& app, const std::vector<HWND>& movedWindows, Utils::PwaHelper& pwaHelper)
{
std::optional<Utils::Apps::AppData> appDataNearest = std::nullopt;
WindowWithDistance nearestWindowWithDistance{};
for (HWND window : m_windowsBefore)
{
if (std::find(movedWindows.begin(), movedWindows.end(), window) != movedWindows.end())
{
continue;
}
std::wstring processPath = get_process_path(window);
if (processPath.empty())
{
continue;
}
DWORD pid{};
GetWindowThreadProcessId(window, &pid);
auto data = Utils::Apps::GetApp(processPath, pid, m_installedApps);
if (!data.has_value())
{
continue;
}
pwaHelper.UpdatePwaApp(&data.value(), window);
if ((app.name == data.value().name || app.path == data.value().installPath) && (app.pwaAppId == data.value().pwaAppId))
{
if (!appDataNearest.has_value())
{
appDataNearest = data;
nearestWindowWithDistance.distance = PlacementHelper::CalculateDistance(app, window);
nearestWindowWithDistance.window = window;
}
else
{
int currentDistance = PlacementHelper::CalculateDistance(app, window);
if (currentDistance < nearestWindowWithDistance.distance)
{
appDataNearest = data;
nearestWindowWithDistance.distance = currentDistance;
nearestWindowWithDistance.window = window;
}
}
}
}
if (appDataNearest.has_value())
{
return nearestWindowWithDistance;
}
return std::nullopt;
}
WindowArranger::WindowArranger(WorkspacesData::WorkspacesProject project) :
m_project(project),
m_windowsBefore(WindowEnumerator::Enumerate(WindowFilter::Filter)),
m_monitors(MonitorUtils::IdentifyMonitors()),
m_installedApps(Utils::Apps::GetAppsList()),
//m_windowCreationHandler(std::bind(&WindowArranger::onWindowCreated, this, std::placeholders::_1)),
m_ipcHelper(IPCHelperStrings::WindowArrangerPipeName, IPCHelperStrings::LauncherArrangerPipeName, std::bind(&WindowArranger::receiveIpcMessage, this, std::placeholders::_1)),
m_launchingStatus(m_project)
{
if (project.moveExistingWindows)
{
Logger::info(L"Moving existing windows");
bool isMovePhase = true;
bool movedAny = false;
std::vector<HWND> movedWindows;
std::vector<WorkspacesData::WorkspacesProject::Application> movedApps;
Utils::PwaHelper pwaHelper{};
while (isMovePhase)
{
isMovePhase = false;
int minDistance = INT_MAX;
WorkspacesData::WorkspacesProject::Application appToMove;
HWND windowToMove = NULL;
for (auto& app : project.apps)
{
// move the apps which are set to "Move-If-Exists" and are already present (launched, running)
if (std::find(movedApps.begin(), movedApps.end(), app) != movedApps.end())
{
continue;
}
std::optional<WindowWithDistance> nearestWindowWithDistance;
nearestWindowWithDistance = GetNearestWindow(app, movedWindows, pwaHelper);
if (nearestWindowWithDistance.has_value())
{
if (nearestWindowWithDistance.value().distance < minDistance)
{
minDistance = nearestWindowWithDistance.value().distance;
appToMove = app;
windowToMove = nearestWindowWithDistance.value().window;
}
}
else
{
Logger::info(L"The app {} is not found at launch, cannot be moved, has to be started", app.name);
movedApps.push_back(app);
}
}
if (minDistance < INT_MAX)
{
isMovePhase = true;
movedAny = true;
bool success = TryMoveWindow(appToMove, windowToMove);
movedApps.push_back(appToMove);
if (success)
{
movedWindows.push_back(windowToMove);
}
}
}
if (movedAny)
{
// Wait if there were moved windows. This message might not arrive if sending immediately after the last "moved" message (status update)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
Logger::info(L"Finished moving existing windows");
}
m_ipcHelper.send(L"ready");
const long maxLaunchingWaitingTime = 10000, maxRepositionWaitingTime = 3000, ms = 300;
@ -138,27 +313,17 @@ WindowArranger::WindowArranger(WorkspacesData::WorkspacesProject project) :
}
}
//void WindowArranger::onWindowCreated(HWND window)
//{
// if (!WindowFilter::Filter(window))
// {
// return;
// }
//
// processWindow(window);
//}
void WindowArranger::processWindows(bool processAll)
{
std::vector<HWND> windows = WindowEnumerator::Enumerate(WindowFilter::Filter);
if (!processAll)
{
std::vector<HWND> windowsDiff{};
std::copy_if(windows.begin(), windows.end(), std::back_inserter(windowsDiff), [&](HWND window) { return std::find(m_windowsBefore.begin(), m_windowsBefore.end(), window) == m_windowsBefore.end(); });
windows = windowsDiff;
}
for (HWND window : windows)
{
processWindow(window);
@ -194,12 +359,11 @@ void WindowArranger::processWindow(HWND window)
}
const auto& apps = m_launchingStatus.Get();
auto iter = std::find_if(apps.begin(), apps.end(), [&](const auto& val)
{
return val.second.state == LaunchingState::Launched &&
!val.second.window &&
(val.first.name == data.value().name || val.first.path == data.value().installPath);
});
auto iter = std::find_if(apps.begin(), apps.end(), [&](const auto& val) {
return val.second.state == LaunchingState::Launched &&
!val.second.window &&
(val.first.name == data.value().name || val.first.path == data.value().installPath);
});
if (iter == apps.end())
{
@ -258,7 +422,7 @@ bool WindowArranger::moveWindow(HWND window, const WorkspacesData::WorkspacesPro
rect.top = static_cast<long>(std::round(rect.top * mult));
rect.bottom = static_cast<long>(std::round(rect.bottom * mult));
if (FancyZones::SizeWindowToRect(window, currentMonitor, launchMinimized, launchMaximized, rect))
if (PlacementHelper::SizeWindowToRect(window, currentMonitor, launchMinimized, launchMaximized, rect))
{
WorkspacesWindowProperties::StampWorkspacesLaunchedProperty(window);
Logger::trace(L"Placed {} to ({},{}) [{}x{}]", app.name, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);

Просмотреть файл

@ -5,8 +5,15 @@
#include <WorkspacesLib/AppUtils.h>
#include <WorkspacesLib/IPCHelper.h>
#include <WorkspacesLib/LaunchingStatus.h>
#include <WorkspacesLib/PwaHelper.h>
#include <WorkspacesLib/WorkspacesData.h>
struct WindowWithDistance
{
int distance;
HWND window;
};
class WindowArranger
{
public:
@ -21,7 +28,9 @@ private:
//const WindowCreationHandler m_windowCreationHandler;
IPCHelper m_ipcHelper;
LaunchingStatus m_launchingStatus;
std::optional<WindowWithDistance> GetNearestWindow(const WorkspacesData::WorkspacesProject::Application& app, const std::vector<HWND>& movedWindows, Utils::PwaHelper& pwaHelper);
bool TryMoveWindow(const WorkspacesData::WorkspacesProject::Application& app, HWND windowToMove);
//void onWindowCreated(HWND window);
void processWindows(bool processAll);
void processWindow(HWND window);