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  }