Project.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Collections.Generic; 7 using System.ComponentModel; 8 using System.Drawing; 9 using System.Globalization; 10 using System.Linq; 11 using System.Text.Json.Serialization; 12 using System.Threading.Tasks; 13 using System.Windows.Media.Imaging; 14 15 using ManagedCommon; 16 using WorkspacesCsharpLibrary.Data; 17 using WorkspacesEditor.Utils; 18 19 namespace WorkspacesEditor.Models 20 { 21 public class Project : INotifyPropertyChanged 22 { 23 [JsonIgnore] 24 public string EditorWindowTitle { get; set; } 25 26 public string Id { get; private set; } 27 28 private string _name; 29 30 public string Name 31 { 32 get => _name; 33 34 set 35 { 36 _name = value; 37 OnPropertyChanged(new PropertyChangedEventArgs(nameof(Name))); 38 OnPropertyChanged(new PropertyChangedEventArgs(nameof(CanBeSaved))); 39 } 40 } 41 42 public long CreationTime { get; } // in seconds 43 44 public long LastLaunchedTime { get; } // in seconds 45 46 public bool IsShortcutNeeded { get; set; } 47 48 public bool MoveExistingWindows { get; set; } 49 50 public string LastLaunched 51 { 52 get 53 { 54 string lastLaunched = WorkspacesEditor.Properties.Resources.LastLaunched + ": "; 55 if (LastLaunchedTime == 0) 56 { 57 return lastLaunched + WorkspacesEditor.Properties.Resources.Never; 58 } 59 60 const int SECOND = 1; 61 const int MINUTE = 60 * SECOND; 62 const int HOUR = 60 * MINUTE; 63 const int DAY = 24 * HOUR; 64 const int MONTH = 30 * DAY; 65 66 DateTime lastLaunchDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(LastLaunchedTime); 67 68 TimeSpan ts = DateTime.UtcNow - lastLaunchDateTime; 69 double delta = Math.Abs(ts.TotalSeconds); 70 71 if (delta < 1 * MINUTE) 72 { 73 return lastLaunched + WorkspacesEditor.Properties.Resources.Recently; 74 } 75 76 if (delta < 2 * MINUTE) 77 { 78 return lastLaunched + WorkspacesEditor.Properties.Resources.OneMinuteAgo; 79 } 80 81 if (delta < 45 * MINUTE) 82 { 83 return lastLaunched + ts.Minutes + " " + WorkspacesEditor.Properties.Resources.MinutesAgo; 84 } 85 86 if (delta < 90 * MINUTE) 87 { 88 return lastLaunched + WorkspacesEditor.Properties.Resources.OneHourAgo; 89 } 90 91 if (delta < 24 * HOUR) 92 { 93 return lastLaunched + ts.Hours + " " + WorkspacesEditor.Properties.Resources.HoursAgo; 94 } 95 96 if (delta < 48 * HOUR) 97 { 98 return lastLaunched + WorkspacesEditor.Properties.Resources.Yesterday; 99 } 100 101 if (delta < 30 * DAY) 102 { 103 return lastLaunched + ts.Days + " " + WorkspacesEditor.Properties.Resources.DaysAgo; 104 } 105 106 if (delta < 12 * MONTH) 107 { 108 int months = Convert.ToInt32(Math.Floor((double)ts.Days / 30)); 109 return lastLaunched + (months <= 1 ? WorkspacesEditor.Properties.Resources.OneMonthAgo : months + " " + WorkspacesEditor.Properties.Resources.MonthsAgo); 110 } 111 else 112 { 113 int years = Convert.ToInt32(Math.Floor((double)ts.Days / 365)); 114 return lastLaunched + (years <= 1 ? WorkspacesEditor.Properties.Resources.OneYearAgo : years + " " + WorkspacesEditor.Properties.Resources.YearsAgo); 115 } 116 } 117 } 118 119 public bool CanBeSaved => Name.Length > 0 && Applications.Count > 0; 120 121 private bool _isRevertEnabled; 122 123 public bool IsRevertEnabled 124 { 125 get => _isRevertEnabled; 126 set 127 { 128 if (_isRevertEnabled != value) 129 { 130 _isRevertEnabled = value; 131 OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsRevertEnabled))); 132 } 133 } 134 } 135 136 private bool _isPopupVisible; 137 138 [JsonIgnore] 139 public bool IsPopupVisible 140 { 141 get => _isPopupVisible; 142 143 set 144 { 145 _isPopupVisible = value; 146 OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsPopupVisible))); 147 } 148 } 149 150 public List<Application> Applications { get; set; } 151 152 public List<object> ApplicationsListed 153 { 154 get 155 { 156 List<object> applicationsListed = []; 157 ILookup<MonitorSetup, Application> apps = Applications.Where(x => !x.Minimized).ToLookup(x => x.MonitorSetup); 158 foreach (IGrouping<MonitorSetup, Application> appItem in apps.OrderBy(x => x.Key.MonitorDpiUnawareBounds.Left).ThenBy(x => x.Key.MonitorDpiUnawareBounds.Top)) 159 { 160 MonitorHeaderRow headerRow = new() { MonitorName = "Screen " + appItem.Key.MonitorNumber, SelectString = Properties.Resources.SelectAllAppsOnMonitor + " " + appItem.Key.MonitorInfo }; 161 applicationsListed.Add(headerRow); 162 foreach (Application app in appItem) 163 { 164 applicationsListed.Add(app); 165 } 166 } 167 168 IEnumerable<Application> minimizedApps = Applications.Where(x => x.Minimized); 169 if (minimizedApps.Any()) 170 { 171 MonitorHeaderRow headerRow = new() { MonitorName = Properties.Resources.Minimized_Apps, SelectString = Properties.Resources.SelectAllMinimizedApps }; 172 applicationsListed.Add(headerRow); 173 foreach (Application app in minimizedApps) 174 { 175 applicationsListed.Add(app); 176 } 177 } 178 179 return applicationsListed; 180 } 181 } 182 183 [JsonIgnore] 184 public string AppsCountString 185 { 186 get 187 { 188 int count = Applications.Count; 189 return count.ToString(CultureInfo.InvariantCulture) + " " + (count == 1 ? Properties.Resources.App : Properties.Resources.Apps); 190 } 191 } 192 193 public List<MonitorSetup> Monitors { get; } 194 195 public bool IsPositionChangedManually { get; set; } // telemetry 196 197 private BitmapImage _previewIcons; 198 private BitmapImage _previewImage; 199 private double _previewImageWidth; 200 201 public Project(Project selectedProject) 202 { 203 Id = selectedProject.Id; 204 Name = selectedProject.Name; 205 PreviewIcons = selectedProject.PreviewIcons; 206 PreviewImage = selectedProject.PreviewImage; 207 IsShortcutNeeded = selectedProject.IsShortcutNeeded; 208 MoveExistingWindows = selectedProject.MoveExistingWindows; 209 210 int screenIndex = 1; 211 212 Monitors = []; 213 foreach (MonitorSetup item in selectedProject.Monitors.OrderBy(x => x.MonitorDpiAwareBounds.Left).ThenBy(x => x.MonitorDpiAwareBounds.Top)) 214 { 215 Monitors.Add(item); 216 screenIndex++; 217 } 218 219 Applications = []; 220 foreach (Application item in selectedProject.Applications) 221 { 222 Application newApp = new(item); 223 newApp.Parent = this; 224 newApp.InitializationFinished(); 225 Applications.Add(newApp); 226 } 227 } 228 229 public Project(ProjectWrapper project) 230 { 231 Id = project.Id; 232 Name = project.Name; 233 CreationTime = project.CreationTime; 234 LastLaunchedTime = project.LastLaunchedTime; 235 IsShortcutNeeded = project.IsShortcutNeeded; 236 MoveExistingWindows = project.MoveExistingWindows; 237 Monitors = []; 238 Applications = []; 239 240 foreach (ApplicationWrapper app in project.Applications) 241 { 242 Models.Application newApp = new() 243 { 244 Id = string.IsNullOrEmpty(app.Id) ? $"{{{Guid.NewGuid()}}}" : app.Id, 245 AppName = app.Application, 246 AppPath = app.ApplicationPath, 247 AppTitle = app.Title, 248 PwaAppId = string.IsNullOrEmpty(app.PwaAppId) ? string.Empty : app.PwaAppId, 249 Version = string.IsNullOrEmpty(app.Version) ? string.Empty : app.Version, 250 PackageFullName = app.PackageFullName, 251 AppUserModelId = app.AppUserModelId, 252 Parent = this, 253 CommandLineArguments = app.CommandLineArguments, 254 IsElevated = app.IsElevated, 255 CanLaunchElevated = app.CanLaunchElevated, 256 Maximized = app.Maximized, 257 Minimized = app.Minimized, 258 IsNotFound = false, 259 Position = new Models.Application.WindowPosition() 260 { 261 Height = app.Position.Height, 262 Width = app.Position.Width, 263 X = app.Position.X, 264 Y = app.Position.Y, 265 }, 266 MonitorNumber = app.Monitor, 267 }; 268 newApp.InitializationFinished(); 269 Applications.Add(newApp); 270 } 271 272 foreach (MonitorConfigurationWrapper monitor in project.MonitorConfiguration) 273 { 274 System.Windows.Rect dpiAware = new(monitor.MonitorRectDpiAware.Left, monitor.MonitorRectDpiAware.Top, monitor.MonitorRectDpiAware.Width, monitor.MonitorRectDpiAware.Height); 275 System.Windows.Rect dpiUnaware = new(monitor.MonitorRectDpiUnaware.Left, monitor.MonitorRectDpiUnaware.Top, monitor.MonitorRectDpiUnaware.Width, monitor.MonitorRectDpiUnaware.Height); 276 Monitors.Add(new MonitorSetup(monitor.Id, monitor.InstanceId, monitor.MonitorNumber, monitor.Dpi, dpiAware, dpiUnaware)); 277 } 278 } 279 280 public BitmapImage PreviewIcons 281 { 282 get => _previewIcons; 283 284 set 285 { 286 _previewIcons = value; 287 OnPropertyChanged(new PropertyChangedEventArgs(nameof(PreviewIcons))); 288 } 289 } 290 291 public BitmapImage PreviewImage 292 { 293 get => _previewImage; 294 295 set 296 { 297 _previewImage = value; 298 OnPropertyChanged(new PropertyChangedEventArgs(nameof(PreviewImage))); 299 } 300 } 301 302 public double PreviewImageWidth 303 { 304 get => _previewImageWidth; 305 306 set 307 { 308 _previewImageWidth = value; 309 OnPropertyChanged(new PropertyChangedEventArgs(nameof(PreviewImageWidth))); 310 } 311 } 312 313 public event PropertyChangedEventHandler PropertyChanged; 314 315 public void OnPropertyChanged(PropertyChangedEventArgs e) 316 { 317 PropertyChanged?.Invoke(this, e); 318 } 319 320 public async void Initialize(Theme currentTheme) 321 { 322 PreviewIcons = await Task.Run(() => DrawHelper.DrawPreviewIcons(this)); 323 Rectangle commonBounds = GetCommonBounds(); 324 PreviewImage = await Task.Run(() => DrawHelper.DrawPreview(this, commonBounds, currentTheme)); 325 PreviewImageWidth = commonBounds.Width / (commonBounds.Height * 1.2 / 200); 326 } 327 328 private Rectangle GetCommonBounds() 329 { 330 double minX = Monitors.First().MonitorDpiAwareBounds.Left; 331 double minY = Monitors.First().MonitorDpiAwareBounds.Top; 332 double maxX = Monitors.First().MonitorDpiAwareBounds.Right; 333 double maxY = Monitors.First().MonitorDpiAwareBounds.Bottom; 334 for (int monitorIndex = 1; monitorIndex < Monitors.Count; monitorIndex++) 335 { 336 Monitor monitor = Monitors[monitorIndex]; 337 minX = Math.Min(minX, monitor.MonitorDpiAwareBounds.Left); 338 minY = Math.Min(minY, monitor.MonitorDpiAwareBounds.Top); 339 maxX = Math.Max(maxX, monitor.MonitorDpiAwareBounds.Right); 340 maxY = Math.Max(maxY, monitor.MonitorDpiAwareBounds.Bottom); 341 } 342 343 return new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY)); 344 } 345 346 public void UpdateAfterLaunchAndEdit(Project projectBeforeLaunch) 347 { 348 Id = projectBeforeLaunch.Id; 349 Name = projectBeforeLaunch.Name; 350 IsRevertEnabled = true; 351 MoveExistingWindows = projectBeforeLaunch.MoveExistingWindows; 352 foreach (Application app in Applications) 353 { 354 var sameAppBefore = projectBeforeLaunch.Applications.Where(x => x.Id.Equals(app.Id, StringComparison.OrdinalIgnoreCase)); 355 if (sameAppBefore.Any()) 356 { 357 var appBefore = sameAppBefore.FirstOrDefault(); 358 app.CommandLineArguments = appBefore.CommandLineArguments; 359 app.IsElevated = appBefore.IsElevated; 360 } 361 } 362 } 363 364 internal void CloseExpanders() 365 { 366 foreach (Application app in Applications) 367 { 368 app.IsExpanded = false; 369 } 370 } 371 372 internal MonitorSetup GetMonitorForApp(Application app) 373 { 374 MonitorSetup monitorSetup = Monitors.Where(x => x.MonitorNumber == app.MonitorNumber).FirstOrDefault(); 375 if (monitorSetup == null) 376 { 377 // monitors changed: try to determine monitor id based on middle point 378 int middleX = app.Position.X + (app.Position.Width / 2); 379 int middleY = app.Position.Y + (app.Position.Height / 2); 380 MonitorSetup monitorCandidate = Monitors.Where(x => 381 (x.MonitorDpiUnawareBounds.Left < middleX) && 382 (x.MonitorDpiUnawareBounds.Right > middleX) && 383 (x.MonitorDpiUnawareBounds.Top < middleY) && 384 (x.MonitorDpiUnawareBounds.Bottom > middleY)).FirstOrDefault(); 385 if (monitorCandidate != null) 386 { 387 app.MonitorNumber = monitorCandidate.MonitorNumber; 388 return monitorCandidate; 389 } 390 else 391 { 392 // monitors and even the app's area unknown, set the main monitor (which is closer to (0,0)) as the app's monitor 393 monitorCandidate = Monitors.OrderBy(x => Math.Abs(x.MonitorDpiUnawareBounds.Left) + Math.Abs(x.MonitorDpiUnawareBounds.Top)).FirstOrDefault(); 394 if (monitorCandidate != null) 395 { 396 app.MonitorNumber = monitorCandidate.MonitorNumber; 397 return monitorCandidate; 398 } 399 else 400 { 401 // no monitors defined at all. 402 Logger.LogError($"Wrong workspace setup. No monitors defined for the workspace: {Name}."); 403 return null; 404 } 405 } 406 } 407 408 return monitorSetup; 409 } 410 } 411 }