/
githubmirror
/
PowerToys
Обзор
Документация
Войти
/
githubmirror
/
PowerToys
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/modules/Workspaces/WorkspacesEditor/ViewModels/MainViewModel.cs
612 строк
22 KB
Niels Laute
[Workspaces] Move to WPF Fluent and tweak overall UX (#46172)
21 май 2026, 08:54
Не верифицирован
21 май 2026, 08:54
52bf042
Код
Авторство
О чём код?
// 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.Drawing; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Timers; using System.Windows; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Telemetry; using WorkspacesCsharpLibrary; using WorkspacesCsharpLibrary.Data; using WorkspacesCsharpLibrary.Utils; using WorkspacesEditor.Models; using WorkspacesEditor.Telemetry; using WorkspacesEditor.Utils; using static WorkspacesCsharpLibrary.Data.WorkspacesData; namespace WorkspacesEditor.ViewModels { public class MainViewModel : INotifyPropertyChanged, IDisposable { private WorkspacesEditorIO _workspacesEditorIO; private ProjectEditor editPage; private SnapshotWindow _snapshotWindow; private List<OverlayWindow> _overlayWindows = new List<OverlayWindow>(); private Project editedProject; private Project projectBeforeLaunch; private string projectNameBeingEdited; private MainWindow _mainWindow; private Timer lastUpdatedTimer; private WorkspacesSettings settings; private PwaHelper _pwaHelper; private bool _isExistingProjectLaunched; public ObservableCollection<Project> Workspaces { get; set; } = new ObservableCollection<Project>(); public IEnumerable<Project> WorkspacesView { get { IEnumerable<Project> workspaces = GetFilteredWorkspaces(); IsWorkspacesViewEmpty = !(workspaces != null && workspaces.Any()); OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsWorkspacesViewEmpty))); if (IsWorkspacesViewEmpty) { if (Workspaces != null && Workspaces.Any()) { EmptyWorkspacesViewMessage = Properties.Resources.NoWorkspacesMatch; } else { EmptyWorkspacesViewMessage = Properties.Resources.No_Workspaces_Message; } OnPropertyChanged(new PropertyChangedEventArgs(nameof(EmptyWorkspacesViewMessage))); return Enumerable.Empty<Project>(); } OrderBy orderBy = (OrderBy)_orderByIndex; if (orderBy == OrderBy.LastViewed) { return workspaces.OrderByDescending(x => x.LastLaunchedTime); } else if (orderBy == OrderBy.Created) { return workspaces.OrderByDescending(x => x.CreationTime); } else { return workspaces.OrderBy(x => x.Name); } } } public bool IsWorkspacesViewEmpty { get; set; } public string EmptyWorkspacesViewMessage { get; set; } // return those workspaces where the project name or any of the selected apps' name contains the search term private IEnumerable<Project> GetFilteredWorkspaces() { if (string.IsNullOrEmpty(_searchTerm)) { return Workspaces; } return Workspaces.Where(x => { if (x.Name.Contains(_searchTerm, StringComparison.InvariantCultureIgnoreCase)) { return true; } if (x.Applications == null) { return false; } return x.Applications.Any(app => app.AppName.Contains(_searchTerm, StringComparison.InvariantCultureIgnoreCase)); }); } private string _searchTerm; public string SearchTerm { get => _searchTerm; set { _searchTerm = value; OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView))); } } private int _orderByIndex; public int OrderByIndex { get => _orderByIndex; set { _orderByIndex = value; OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView))); settings.Properties.SortBy = (WorkspacesProperties.SortByProperty)value; settings.Save(SettingsUtils.Default); } } public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged(PropertyChangedEventArgs e) { PropertyChanged?.Invoke(this, e); } public MainViewModel(WorkspacesEditorIO workspacesEditorIO) { settings = Utils.Settings.ReadSettings(); _orderByIndex = (int)settings.Properties.SortBy; _workspacesEditorIO = workspacesEditorIO; _pwaHelper = new PwaHelper(); lastUpdatedTimer = new System.Timers.Timer(); lastUpdatedTimer.Interval = 1000; lastUpdatedTimer.Elapsed += LastUpdatedTimerElapsed; lastUpdatedTimer.Start(); } public void Initialize() { foreach (Project project in Workspaces) { project.Initialize(App.GetCurrentTheme()); } OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView))); } public void SetEditedProject(Project editedProject) { this.editedProject = editedProject; } public void SaveProject(Project projectToSave) { SendEditTelemetryEvent(projectToSave, editedProject); if (editedProject.Name != projectToSave.Name) { RemoveShortcut(editedProject); } editedProject.Name = projectToSave.Name; editedProject.IsShortcutNeeded = projectToSave.IsShortcutNeeded; editedProject.MoveExistingWindows = projectToSave.MoveExistingWindows; editedProject.PreviewIcons = projectToSave.PreviewIcons; editedProject.PreviewImage = projectToSave.PreviewImage; editedProject.Applications = projectToSave.Applications.Where(x => x.IsIncluded).ToList(); editedProject.OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("AppsCountString")); editedProject.Initialize(App.GetCurrentTheme()); _workspacesEditorIO.SerializeWorkspaces(Workspaces.ToList()); ApplyShortcut(editedProject); } private string GetDesktopShortcutAddress(Project project) => Path.Combine(FolderUtils.Desktop(), project.Name + ".lnk"); private string GetShortcutStoreAddress(Project project) { var dataFolder = FolderUtils.DataFolder(); Directory.CreateDirectory(dataFolder); var shortcutStoreFolder = Path.Combine(dataFolder, "WorkspacesIcons"); Directory.CreateDirectory(shortcutStoreFolder); return Path.Combine(shortcutStoreFolder, project.Id + ".ico"); } private void ApplyShortcut(Project project) { if (!project.IsShortcutNeeded) { RemoveShortcut(project); return; } var basePath = AppDomain.CurrentDomain.BaseDirectory; var shortcutAddress = GetDesktopShortcutAddress(project); var shortcutIconFilename = GetShortcutStoreAddress(project); Bitmap icon = WorkspacesIcon.DrawIcon(WorkspacesIcon.IconTextFromProjectName(project.Name), App.GetCurrentTheme()); WorkspacesIcon.SaveIcon(icon, shortcutIconFilename); try { // Workaround to be able to create a shortcut with unicode filename File.WriteAllBytes(shortcutAddress, Array.Empty<byte>()); // Create a ShellLinkObject that references the .lnk file Shell32.Shell shell = new Shell32.Shell(); Shell32.Folder dir = shell.NameSpace(FolderUtils.Desktop()); Shell32.FolderItem folderItem = dir.Items().Item($"{project.Name}.lnk"); Shell32.ShellLinkObject link = (Shell32.ShellLinkObject)folderItem.GetLink; // Set the .lnk file properties link.Description = $"Project Launcher {project.Id}"; link.Path = Path.Combine(basePath, "PowerToys.WorkspacesLauncher.exe"); link.Arguments = $"{project.Id.ToString()} {(int)InvokePoint.Shortcut}"; link.WorkingDirectory = basePath; link.SetIconLocation(shortcutIconFilename, 0); link.Save(shortcutAddress); } catch (Exception ex) { Logger.LogError($"Shortcut creation error: {ex.Message}"); } } public void SaveProjectName(Project project) { projectNameBeingEdited = project.Name; } public void CancelProjectName(Project project) { project.Name = projectNameBeingEdited; } public async void SnapWorkspace() { CancelSnapshot(); await Task.Run(() => RunSnapshotTool(_isExistingProjectLaunched)); Project project = _workspacesEditorIO.ParseTempProject(); if (project != null) { if (_isExistingProjectLaunched) { project.UpdateAfterLaunchAndEdit(projectBeforeLaunch); project.EditorWindowTitle = Properties.Resources.EditWorkspace; editPage.DataContext = project; CheckShortcutPresence(project); project.Initialize(App.GetCurrentTheme()); } else { EditProject(project, true); } } } internal void RevertLaunch() { CheckShortcutPresence(projectBeforeLaunch); editPage.DataContext = projectBeforeLaunch; projectBeforeLaunch.Initialize(App.GetCurrentTheme()); } public void EditProject(Project selectedProject, bool isNewlyCreated = false) { editPage = new ProjectEditor(this); SetEditedProject(selectedProject); if (!isNewlyCreated) { selectedProject = new Project(selectedProject); } if (isNewlyCreated) { // generate a default name for the new project string defaultNamePrefix = Properties.Resources.DefaultWorkspaceNamePrefix; int nextProjectIndex = 0; foreach (var proj in Workspaces) { if (proj.Name.StartsWith(defaultNamePrefix, StringComparison.CurrentCulture)) { try { int index = int.Parse(proj.Name[(defaultNamePrefix.Length + 1)..], CultureInfo.CurrentCulture); if (nextProjectIndex < index) { nextProjectIndex = index; } } catch (Exception) { } } } selectedProject.Name = defaultNamePrefix + " " + (nextProjectIndex + 1).ToString(CultureInfo.CurrentCulture); } selectedProject.EditorWindowTitle = isNewlyCreated ? Properties.Resources.CreateWorkspace : Properties.Resources.EditWorkspace; selectedProject.Initialize(App.GetCurrentTheme()); CheckShortcutPresence(selectedProject); editPage.DataContext = selectedProject; _mainWindow.ShowPage(editPage); lastUpdatedTimer.Stop(); } private void CheckShortcutPresence(Project project) { string basePath = AppDomain.CurrentDomain.BaseDirectory; string shortcutAddress = Path.Combine(FolderUtils.Desktop(), project.Name + ".lnk"); project.IsShortcutNeeded = File.Exists(shortcutAddress); } public void AddNewProject(Project project) { project.Applications.RemoveAll(app => !app.IsIncluded); project.Initialize(App.GetCurrentTheme()); Workspaces.Add(project); _workspacesEditorIO.SerializeWorkspaces(Workspaces.ToList()); TempProjectData.DeleteTempFile(); OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView))); ApplyShortcut(project); SendCreateTelemetryEvent(project); } public void DeleteProject(Project selectedProject) { Workspaces.Remove(selectedProject); _workspacesEditorIO.SerializeWorkspaces(Workspaces.ToList()); RemoveShortcut(selectedProject); OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView))); SendDeleteTelemetryEvent(); } private void RemoveShortcut(Project selectedProject) { string shortcutAddress = GetDesktopShortcutAddress(selectedProject); string shortcutIconFilename = GetShortcutStoreAddress(selectedProject); if (File.Exists(shortcutIconFilename)) { File.Delete(shortcutIconFilename); } if (File.Exists(shortcutAddress)) { File.Delete(shortcutAddress); } } public void SetMainWindow(MainWindow mainWindow) { _mainWindow = mainWindow; } public void SwitchToMainView() { _mainWindow.SwitchToMainView(); SearchTerm = string.Empty; OnPropertyChanged(new PropertyChangedEventArgs(nameof(SearchTerm))); lastUpdatedTimer.Start(); editedProject = null; } public void LaunchProject(string projectId) { if (!Workspaces.Where(x => x.Id == projectId).Any()) { Logger.LogWarning($"Workspace to launch not found. Id: {projectId}"); return; } LaunchProject(Workspaces.Where(x => x.Id == projectId).First(), true); } public async void LaunchProject(Project project, bool exitAfterLaunch = false) { if (project == null) { return; } await Task.Run(() => RunLauncher(project.Id, InvokePoint.EditorButton)); if (_workspacesEditorIO.ParseWorkspaces(this).Result == true) { OnPropertyChanged(new PropertyChangedEventArgs(nameof(WorkspacesView))); } if (exitAfterLaunch) { Logger.LogInfo($"Launched the Workspace {project.Name}. Exiting."); Environment.Exit(0); } } private void LastUpdatedTimerElapsed(object sender, ElapsedEventArgs e) { if (Workspaces == null) { return; } foreach (Project project in Workspaces) { project.OnPropertyChanged(new PropertyChangedEventArgs("LastLaunched")); } } private void RunSnapshotTool(bool isExistingProjectLaunched) { Process process = new Process(); var exeDir = Path.GetDirectoryName(Environment.ProcessPath); var snapshotUtilsPath = Path.Combine(exeDir, "PowerToys.WorkspacesSnapshotTool.exe"); process.StartInfo = new ProcessStartInfo(snapshotUtilsPath); process.StartInfo.CreateNoWindow = true; process.StartInfo.Arguments = isExistingProjectLaunched ? $"{(int)InvokePoint.LaunchAndEdit}" : string.Empty; try { process.Start(); process.WaitForExit(); } catch (Exception ex) { MessageBox.Show($"An error occurred: {ex.Message}"); } } private void RunLauncher(string projectId, InvokePoint invokePoint) { Process process = new Process(); process.StartInfo = new ProcessStartInfo(@".\PowerToys.WorkspacesLauncher.exe", $"{projectId} {(int)invokePoint}"); process.StartInfo.CreateNoWindow = true; try { process.Start(); process.WaitForExit(); } catch (Exception ex) { MessageBox.Show($"An error occurred: {ex.Message}"); } } public void Dispose() { GC.SuppressFinalize(this); } internal void CloseAllPopups() { foreach (Project project in Workspaces) { project.IsPopupVisible = false; } } internal void EnterSnapshotMode(bool isExistingProjectLaunched) { _isExistingProjectLaunched = isExistingProjectLaunched; _mainWindow.WindowState = System.Windows.WindowState.Minimized; _overlayWindows.Clear(); foreach (var screen in MonitorHelper.GetDpiUnawareScreens()) { var bounds = screen.Bounds; OverlayWindow overlayWindow = new OverlayWindow(); // Use DPI-unaware positioning to fix overlay on mixed-DPI multi-monitor setups overlayWindow.SetTargetBounds(bounds.Left, bounds.Top, bounds.Width, bounds.Height); overlayWindow.ShowActivated = true; overlayWindow.Topmost = true; overlayWindow.Show(); _overlayWindows.Add(overlayWindow); } _snapshotWindow = new SnapshotWindow(this); _snapshotWindow.ShowActivated = true; _snapshotWindow.Topmost = true; _snapshotWindow.Show(); } internal void CancelSnapshot() { foreach (OverlayWindow overlayWindow in _overlayWindows) { overlayWindow.Close(); } _mainWindow.WindowState = System.Windows.WindowState.Normal; } internal async void LaunchAndEdit(Project project) { await Task.Run(() => RunLauncher(project.Id, InvokePoint.LaunchAndEdit)); projectBeforeLaunch = new Project(project); EnterSnapshotMode(true); } private void SendCreateTelemetryEvent(Project project) { var telemetryEvent = new CreateEvent(); telemetryEvent.Successful = true; telemetryEvent.NumScreens = project.Monitors.Count; telemetryEvent.AppCount = project.Applications.Count; telemetryEvent.CliCount = project.Applications.FindAll(app => app.CommandLineArguments.Length > 0).Count; telemetryEvent.ShortcutCreated = project.IsShortcutNeeded; telemetryEvent.AdminCount = project.Applications.FindAll(app => app.IsElevated).Count; PowerToysTelemetry.Log.WriteEvent(telemetryEvent); } private void SendEditTelemetryEvent(Project updatedProject, Project prevProject) { int appsRemovedCount = updatedProject.Applications.FindAll(val => !val.IsIncluded).Count; foreach (var app in prevProject.Applications) { var updatedApp = updatedProject.Applications.Find(val => app.AppName == val.AppName && app.Position == val.Position); if (updatedApp == null) { appsRemovedCount++; } } int appsAddedCount = 0; int cliAdded = 0, cliRemoved = 0; int adminAdded = 0, adminRemoved = 0; foreach (var app in updatedProject.Applications) { var prevApp = prevProject.Applications.Find(val => app.AppName == val.AppName && app.Position == val.Position); if (prevApp == null) { if (app.IsIncluded) { appsAddedCount++; } continue; } if (app.CommandLineArguments.Length > 0 && prevApp.CommandLineArguments.Length == 0) { cliAdded++; } if (prevApp.CommandLineArguments.Length > 0 && app.CommandLineArguments.Length == 0) { cliRemoved++; } if (app.IsElevated && !prevApp.IsElevated) { adminAdded++; } if (!app.IsElevated && prevApp.IsElevated) { adminRemoved++; } } var telemetryEvent = new EditEvent(); telemetryEvent.Successful = true; telemetryEvent.ScreenCountDelta = updatedProject.Monitors.Count - prevProject.Monitors.Count; telemetryEvent.AppsAdded = appsAddedCount; telemetryEvent.AppsRemoved = appsRemovedCount; telemetryEvent.CliAdded = cliAdded; telemetryEvent.CliRemoved = cliRemoved; telemetryEvent.AdminAdded = adminAdded; telemetryEvent.AdminRemoved = adminRemoved; telemetryEvent.PixelAdjustmentsUsed = updatedProject.IsPositionChangedManually; PowerToysTelemetry.Log.WriteEvent(telemetryEvent); } private void SendDeleteTelemetryEvent() { var telemetryEvent = new EditEvent(); telemetryEvent.Successful = true; PowerToysTelemetry.Log.WriteEvent(telemetryEvent); } } }