1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

UI - Avoid opening the main window multiple times from tray

Profiling - Thread safety and use high precision counters
Profiling - Profile timed updates
Timed updates - Added argument to give timed updates a name
Plugins - Affix plugin folders with a part of the plugin GUID
Debugger - Added 95th percentile column to profiling
Debugger - Fix scrolling in performance profile tab when hovering over datagrids
This commit is contained in:
Robert 2021-05-17 17:26:49 +02:00
parent 4ab8459927
commit 1660519bee
14 changed files with 315 additions and 185 deletions

View File

@ -8,40 +8,6 @@ namespace Artemis.Core
/// </summary>
public abstract class DataModelPluginFeature : PluginFeature
{
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="action" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="action">
/// The action to call every time the interval has passed. The delta time parameter represents the
/// time passed since the last update in seconds
/// </param>
/// <returns>The resulting plugin update registration which can be used to stop the update</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action<double> action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
return new TimedUpdateRegistration(this, interval, action);
}
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="asyncAction" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="asyncAction">
/// The async action to call every time the interval has passed. The delta time parameter
/// represents the time passed since the last update in seconds
/// </param>
/// <returns>The resulting plugin update registration</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func<double, Task> asyncAction)
{
if (asyncAction == null)
throw new ArgumentNullException(nameof(asyncAction));
return new TimedUpdateRegistration(this, interval, asyncAction);
}
}
}

View File

@ -146,6 +146,15 @@ namespace Artemis.Core
return profiler;
}
/// <summary>
/// Removes a profiler from the plugin
/// </summary>
/// <param name="profiler">The profiler to remove</param>
public void RemoveProfiler(Profiler profiler)
{
_profilers.Remove(profiler);
}
/// <inheritdoc />
public override string ToString()
{

View File

@ -51,16 +51,6 @@ namespace Artemis.Core
/// </summary>
public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable
/// <summary>
/// Gets the last measured update time of the feature
/// </summary>
public TimeSpan UpdateTime { get; private set; }
/// <summary>
/// Gets the last measured render time of the feature
/// </summary>
public TimeSpan RenderTime { get; private set; }
internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction
/// <summary>
@ -241,5 +231,47 @@ namespace Artemis.Core
}
#endregion
#region Timed updates
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="action" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="action">
/// The action to call every time the interval has passed. The delta time parameter represents the
/// time passed since the last update in seconds
/// </param>
/// <param name="name">An optional name used in exceptions and profiling</param>
/// <returns>The resulting plugin update registration which can be used to stop the update</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action<double> action, string? name = null)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
return new TimedUpdateRegistration(this, interval, action, name);
}
/// <summary>
/// Registers a timed update that whenever the plugin is enabled calls the provided <paramref name="asyncAction" /> at the
/// provided
/// <paramref name="interval" />
/// </summary>
/// <param name="interval">The interval at which the update should occur</param>
/// <param name="asyncAction">
/// The async action to call every time the interval has passed. The delta time parameter
/// represents the time passed since the last update in seconds
/// </param>
/// <param name="name">An optional name used in exceptions and profiling</param>
/// <returns>The resulting plugin update registration</returns>
public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func<double, Task> asyncAction, string? name = null)
{
if (asyncAction == null)
throw new ArgumentNullException(nameof(asyncAction));
return new TimedUpdateRegistration(this, interval, asyncAction, name);
}
#endregion
}
}

View File

@ -124,6 +124,8 @@ namespace Artemis.Core
/// <inheritdoc />
public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet());
internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}";
/// <inheritdoc />
public override string ToString()
{

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Artemis.Core
{
@ -35,13 +37,16 @@ namespace Artemis.Core
/// <param name="identifier">A unique identifier for this measurement</param>
public void StartMeasurement(string identifier)
{
if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
lock (Measurements)
{
measurement = new ProfilingMeasurement(identifier);
Measurements.Add(identifier, measurement);
}
if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
{
measurement = new ProfilingMeasurement(identifier);
Measurements.Add(identifier, measurement);
}
measurement.Start();
measurement.Start();
}
}
/// <summary>
@ -51,13 +56,17 @@ namespace Artemis.Core
/// <returns>The number of ticks that passed since the <see cref="StartMeasurement" /> call with the same identifier</returns>
public long StopMeasurement(string identifier)
{
if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
long lockRequestedAt = Stopwatch.GetTimestamp();
lock (Measurements)
{
measurement = new ProfilingMeasurement(identifier);
Measurements.Add(identifier, measurement);
}
if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement))
{
measurement = new ProfilingMeasurement(identifier);
Measurements.Add(identifier, measurement);
}
return measurement.Stop();
return measurement.Stop(Stopwatch.GetTimestamp() - lockRequestedAt);
}
}
/// <summary>
@ -66,7 +75,10 @@ namespace Artemis.Core
/// <param name="identifier"></param>
public void ClearMeasurements(string identifier)
{
Measurements.Remove(identifier);
lock (Measurements)
{
Measurements.Remove(identifier);
}
}
}
}

View File

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Artemis.Core
@ -11,7 +13,8 @@ namespace Artemis.Core
private bool _filledArray;
private int _index;
private long _last;
private DateTime? _start;
private bool _open;
private long _start;
internal ProfilingMeasurement(string identifier)
{
@ -33,22 +36,24 @@ namespace Artemis.Core
/// </summary>
public void Start()
{
_start = DateTime.UtcNow;
_start = Stopwatch.GetTimestamp();
_open = true;
}
/// <summary>
/// Stops measuring time and stores the time passed in the <see cref="Measurements" /> list
/// </summary>
/// <param name="correction">An optional correction in ticks to subtract from the measurement</param>
/// <returns>The time passed since the last <see cref="Start" /> call</returns>
public long Stop()
public long Stop(long correction = 0)
{
if (_start == null)
if (!_open)
return 0;
long difference = (DateTime.UtcNow - _start.Value).Ticks;
long difference = Stopwatch.GetTimestamp() - _start - correction;
_open = false;
Measurements[_index] = difference;
_start = null;
_index++;
if (_index >= 1000)
{
@ -106,5 +111,31 @@ namespace Artemis.Core
? new TimeSpan(Measurements.Max())
: new TimeSpan(Measurements.Take(_index).Max());
}
/// <summary>
/// Gets the nth percentile of the last 1000 measurements
/// </summary>
public TimeSpan GetPercentile(double percentile)
{
if (!_filledArray && _index == 0)
return TimeSpan.Zero;
long[] collection = _filledArray
? Measurements.OrderBy(l => l).ToArray()
: Measurements.Take(_index).OrderBy(l => l).ToArray();
return new TimeSpan((long) Percentile(collection, percentile));
}
private static double Percentile(long[] elements, double percentile)
{
Array.Sort(elements);
double realIndex = percentile * (elements.Length - 1);
int index = (int) realIndex;
double frac = realIndex - index;
if (index + 1 < elements.Length)
return elements[index] * (1 - frac) + elements[index + 1] * frac;
return elements[index];
}
}
}

View File

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using System.Timers;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Humanizer;
using Ninject;
using Serilog;
@ -13,19 +14,20 @@ namespace Artemis.Core
/// </summary>
public class TimedUpdateRegistration : IDisposable
{
private DateTime _lastEvent;
private Timer? _timer;
private bool _disposed;
private readonly object _lock = new();
private ILogger _logger;
private bool _disposed;
private DateTime _lastEvent;
private readonly ILogger _logger;
private Timer? _timer;
internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Action<double> action)
internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Action<double> action, string? name)
{
_logger = CoreService.Kernel.Get<ILogger>();
Feature = feature;
Interval = interval;
Action = action;
Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}";
Feature.Enabled += FeatureOnEnabled;
Feature.Disabled += FeatureOnDisabled;
@ -33,13 +35,14 @@ namespace Artemis.Core
Start();
}
internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func<double, Task> asyncAction)
internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func<double, Task> asyncAction, string? name)
{
_logger = CoreService.Kernel.Get<ILogger>();
Feature = feature;
Interval = interval;
AsyncAction = asyncAction;
Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}";
Feature.Enabled += FeatureOnEnabled;
Feature.Disabled += FeatureOnDisabled;
@ -69,7 +72,12 @@ namespace Artemis.Core
public Func<double, Task>? AsyncAction { get; }
/// <summary>
/// Starts calling the <see cref="Action" /> or <see cref="AsyncAction"/> at the configured <see cref="Interval" />
/// Gets the name of this timed update
/// </summary>
public string Name { get; }
/// <summary>
/// Starts calling the <see cref="Action" /> or <see cref="AsyncAction" /> at the configured <see cref="Interval" />
/// <para>Note: Called automatically when the plugin enables</para>
/// </summary>
public void Start()
@ -93,7 +101,7 @@ namespace Artemis.Core
}
/// <summary>
/// Stops calling the <see cref="Action" /> or <see cref="AsyncAction"/> at the configured <see cref="Interval" />
/// Stops calling the <see cref="Action" /> or <see cref="AsyncAction" /> at the configured <see cref="Interval" />
/// <para>Note: Called automatically when the plugin disables</para>
/// </summary>
public void Stop()
@ -113,49 +121,6 @@ namespace Artemis.Core
}
}
private void TimerOnElapsed(object? sender, ElapsedEventArgs e)
{
if (!Feature.IsEnabled)
return;
lock (_lock)
{
TimeSpan interval = DateTime.Now - _lastEvent;
_lastEvent = DateTime.Now;
// Modules don't always want to update, honor that
if (Feature is Module module && !module.IsUpdateAllowed)
return;
try
{
if (Action != null)
Action(interval.TotalSeconds);
else if (AsyncAction != null)
{
Task task = AsyncAction(interval.TotalSeconds);
task.Wait();
}
}
catch (Exception exception)
{
_logger.Error(exception, "Timed update uncaught exception in plugin {plugin}", Feature.Plugin);
}
}
}
private void FeatureOnEnabled(object? sender, EventArgs e)
{
Start();
}
private void FeatureOnDisabled(object? sender, EventArgs e)
{
Stop();
}
#region IDisposable
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
@ -176,6 +141,55 @@ namespace Artemis.Core
}
}
private void TimerOnElapsed(object? sender, ElapsedEventArgs e)
{
if (!Feature.IsEnabled)
return;
lock (_lock)
{
Feature.Profiler.StartMeasurement(ToString());
TimeSpan interval = DateTime.Now - _lastEvent;
_lastEvent = DateTime.Now;
// Modules don't always want to update, honor that
if (Feature is Module module && !module.IsUpdateAllowed)
return;
try
{
if (Action != null)
{
Action(interval.TotalSeconds);
}
else if (AsyncAction != null)
{
Task task = AsyncAction(interval.TotalSeconds);
task.Wait();
}
}
catch (Exception exception)
{
_logger.Error(exception, "{timedUpdate} uncaught exception in plugin {plugin}", this, Feature.Plugin);
}
finally
{
Feature.Profiler.StopMeasurement(ToString());
}
}
}
private void FeatureOnEnabled(object? sender, EventArgs e)
{
Start();
}
private void FeatureOnDisabled(object? sender, EventArgs e)
{
Stop();
}
/// <inheritdoc />
public void Dispose()
{
@ -183,6 +197,12 @@ namespace Artemis.Core
GC.SuppressFinalize(this);
}
#endregion
/// <inheritdoc />
public sealed override string ToString()
{
if (Interval.TotalSeconds >= 1)
return $"{Name} ({Interval.TotalSeconds} sec)";
return $"{Name} ({Interval.TotalMilliseconds} ms)";
}
}
}

View File

@ -39,9 +39,9 @@ namespace Artemis.Core.Services
ProcessQueuedActions();
}
private void CopyBuiltInPlugin(FileInfo zipFileInfo, ZipArchive zipArchive)
private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory)
{
DirectoryInfo pluginDirectory = new(Path.Combine(Constants.DataFolder, "plugins", Path.GetFileNameWithoutExtension(zipFileInfo.Name)));
DirectoryInfo pluginDirectory = new(Path.Combine(Constants.DataFolder, "plugins", targetDirectory));
bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock"));
// Remove the old directory if it exists
@ -81,12 +81,18 @@ namespace Artemis.Core.Services
using StreamReader reader = new(metaDataFileEntry.Open());
PluginInfo builtInPluginInfo = CoreJson.DeserializeObject<PluginInfo>(reader.ReadToEnd())!;
string preferred = builtInPluginInfo.PreferredPluginDirectory;
string oldPreferred = Path.GetFileNameWithoutExtension(zipFile.Name);
// Rename folders to the new format
// TODO: Get rid of this eventually, it's nice to keep around but it's extra IO that's best avoided
if (pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == oldPreferred) != null)
Directory.Move(Path.Combine(pluginDirectory.FullName, oldPreferred), Path.Combine(pluginDirectory.FullName, preferred));
// Find the matching plugin in the plugin folder
DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == Path.GetFileNameWithoutExtension(zipFile.Name));
DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == preferred);
if (match == null)
{
CopyBuiltInPlugin(zipFile, archive);
CopyBuiltInPlugin(archive, preferred);
}
else
{
@ -94,7 +100,7 @@ namespace Artemis.Core.Services
if (!File.Exists(metadataFile))
{
_logger.Debug("Copying missing built-in plugin {builtInPluginInfo}", builtInPluginInfo);
CopyBuiltInPlugin(zipFile, archive);
CopyBuiltInPlugin(archive, preferred);
}
else
{
@ -114,7 +120,7 @@ namespace Artemis.Core.Services
if (builtInPluginInfo.Version > pluginInfo.Version)
{
_logger.Debug("Copying updated built-in plugin from {pluginInfo} to {builtInPluginInfo}", pluginInfo, builtInPluginInfo);
CopyBuiltInPlugin(zipFile, archive);
CopyBuiltInPlugin(archive, preferred);
}
}
catch (Exception e)
@ -342,8 +348,8 @@ namespace Artemis.Core.Services
foreach (Type featureType in featureTypes)
{
// Load the enabled state and if not found, default to true
PluginFeatureEntity featureEntity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ??
new PluginFeatureEntity { IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureType.FullName! };
PluginFeatureEntity featureEntity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ??
new PluginFeatureEntity {IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureType.FullName!};
plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, featureEntity, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute))));
}
@ -502,6 +508,7 @@ namespace Artemis.Core.Services
Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid);
if (existing != null)
{
try
{
RemovePlugin(existing, false);
@ -510,20 +517,14 @@ namespace Artemis.Core.Services
{
throw new ArtemisPluginException("A plugin with the same GUID is already loaded, failed to remove old version", e);
}
string targetDirectory = pluginInfo.Main.Split(".dll")[0].Replace("/", "").Replace("\\", "");
string uniqueTargetDirectory = targetDirectory;
int attempt = 2;
// Find a unique folder
while (pluginDirectory.EnumerateDirectories().Any(d => d.Name == uniqueTargetDirectory))
{
uniqueTargetDirectory = targetDirectory + "-" + attempt;
attempt++;
}
string targetDirectory = pluginInfo.PreferredPluginDirectory;
if (Directory.Exists(Path.Combine(pluginDirectory.FullName, targetDirectory)))
throw new ArtemisPluginException($"A directory for this plugin already exists {Path.Combine(pluginDirectory.FullName, targetDirectory)}");
// Extract everything in the same archive directory to the unique plugin directory
DirectoryInfo directoryInfo = new(Path.Combine(pluginDirectory.FullName, uniqueTargetDirectory));
DirectoryInfo directoryInfo = new(Path.Combine(pluginDirectory.FullName, targetDirectory));
Utilities.CreateAccessibleDirectory(directoryInfo.FullName);
string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, "");
foreach (ZipArchiveEntry zipArchiveEntry in archive.Entries)

View File

@ -9,6 +9,7 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance
private string _last;
private string _max;
private string _min;
private string _percentile;
public PerformanceDebugMeasurementViewModel(ProfilingMeasurement measurement)
{
@ -41,12 +42,19 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance
set => SetAndNotify(ref _max, value);
}
public string Percentile
{
get => _percentile;
set => SetAndNotify(ref _percentile, value);
}
public void Update()
{
Last = Measurement.GetLast().TotalMilliseconds + " ms";
Average = Measurement.GetAverage().TotalMilliseconds + " ms";
Min = Measurement.GetMin().TotalMilliseconds + " ms";
Max = Measurement.GetMax().TotalMilliseconds + " ms";
Percentile = Measurement.GetPercentile(0.95).TotalMilliseconds + " ms";
}
}
}

View File

@ -27,6 +27,7 @@
<materialDesign:DataGridTextColumn Binding="{Binding Min}" Header="Min" />
<materialDesign:DataGridTextColumn Binding="{Binding Max}" Header="Max" />
<materialDesign:DataGridTextColumn Binding="{Binding Average}" Header="Average" />
<materialDesign:DataGridTextColumn Binding="{Binding Percentile}" Header="95th percentile" />
</DataGrid.Columns>
</DataGrid>
</StackPanel>

View File

@ -19,7 +19,7 @@
If you are having performance issues, below you can find out which plugin might be the culprit.
</TextBlock>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden">
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel">
<ItemsControl ItemsSource="{Binding Items}" Margin="0 0 10 0">
<ItemsControl.ItemTemplate>
<DataTemplate>

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Artemis.UI.Screens.Settings.Debug.Tabs.Performance
{
/// <summary>
/// Interaction logic for PerformanceDebugView.xaml
/// </summary>
public partial class PerformanceDebugView : UserControl
{
public PerformanceDebugView()
{
InitializeComponent();
}
private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
ScrollViewer scv = (ScrollViewer)sender;
scv.ScrollToVerticalOffset(scv.VerticalOffset - e.Delta);
e.Handled = true;
}
}
}

View File

@ -10,8 +10,7 @@
<tb:TaskbarIcon IconSource="{Binding Icon}"
MenuActivation="LeftOrRightClick"
PopupActivation="DoubleClick"
DoubleClickCommand="{s:Action TrayBringToForeground}"
TrayBalloonTipClicked="{s:Action OnTrayBalloonTipClicked}">
DoubleClickCommand="{s:Action TrayBringToForeground}">
<tb:TaskbarIcon.TrayToolTip>
<Border Background="{DynamicResource MaterialDesignToolTipBackground}" CornerRadius="2" Padding="5">

View File

@ -26,10 +26,11 @@ namespace Artemis.UI.Screens
private readonly IKernel _kernel;
private readonly ThemeWatcher _themeWatcher;
private readonly IWindowManager _windowManager;
private ImageSource _icon;
private bool _openingMainWindow;
private RootViewModel _rootViewModel;
private SplashViewModel _splashViewModel;
private TaskbarIcon _taskBarIcon;
private ImageSource _icon;
public TrayViewModel(IKernel kernel,
IWindowManager windowManager,
@ -55,7 +56,7 @@ namespace Artemis.UI.Screens
_themeWatcher.AppsThemeChanged += _themeWatcher_AppsThemeChanged;
ApplyColorSchemeSetting();
ApplyTryIconTheme(_themeWatcher.GetSystemTheme());
ApplyTrayIconTheme(_themeWatcher.GetSystemTheme());
windowService.ConfigureMainWindowProvider(this);
bool autoRunning = Bootstrapper.StartupArguments.Contains("--autorun");
@ -63,7 +64,9 @@ namespace Artemis.UI.Screens
bool showOnAutoRun = settingsService.GetSetting("UI.ShowOnStartup", true).Value;
if (autoRunning && !showOnAutoRun || minimized)
{
coreService.Initialized += (_, _) => updateService.AutoUpdate();
}
else
{
ShowSplashScreen();
@ -71,36 +74,55 @@ namespace Artemis.UI.Screens
}
}
public void TrayBringToForeground()
{
if (IsMainWindowOpen)
{
Execute.PostToUIThread(FocusMainWindow);
return;
}
// Initialize the shared UI when first showing the window
if (!UI.Shared.Bootstrapper.Initialized)
UI.Shared.Bootstrapper.Initialize(_kernel);
Execute.OnUIThreadSync(() =>
{
_splashViewModel?.RequestClose();
_splashViewModel = null;
_rootViewModel = _kernel.Get<RootViewModel>();
_rootViewModel.Closed += RootViewModelOnClosed;
_windowManager.ShowWindow(_rootViewModel);
});
OnMainWindowOpened();
}
public ImageSource Icon
{
get => _icon;
set => SetAndNotify(ref _icon, value);
}
public void TrayBringToForeground()
{
if (_openingMainWindow)
return;
try
{
_openingMainWindow = true;
if (IsMainWindowOpen)
{
Execute.OnUIThreadSync(() =>
{
FocusMainWindow();
_openingMainWindow = false;
});
return;
}
// Initialize the shared UI when first showing the window
if (!UI.Shared.Bootstrapper.Initialized)
UI.Shared.Bootstrapper.Initialize(_kernel);
Execute.OnUIThreadSync(() =>
{
_splashViewModel?.RequestClose();
_splashViewModel = null;
_rootViewModel = _kernel.Get<RootViewModel>();
_rootViewModel.Closed += RootViewModelOnClosed;
_windowManager.ShowWindow(_rootViewModel);
IsMainWindowOpen = true;
_openingMainWindow = false;
});
OnMainWindowOpened();
}
finally
{
_openingMainWindow = false;
}
}
public void TrayActivateSidebarItem(string sidebarItem)
{
TrayBringToForeground();
@ -122,14 +144,6 @@ namespace Artemis.UI.Screens
_taskBarIcon = (TaskbarIcon) ((ContentControl) view).Content;
}
public void OnTrayBalloonTipClicked(object sender, EventArgs e)
{
if (!IsMainWindowOpen)
TrayBringToForeground();
else
FocusMainWindow();
}
private void FocusMainWindow()
{
// Wrestle the main window to the front
@ -158,10 +172,15 @@ namespace Artemis.UI.Screens
private void RootViewModelOnClosed(object sender, CloseEventArgs e)
{
if (_rootViewModel != null)
lock (this)
{
_rootViewModel.Closed -= RootViewModelOnClosed;
_rootViewModel = null;
if (_rootViewModel != null)
{
_rootViewModel.Closed -= RootViewModelOnClosed;
_rootViewModel = null;
}
IsMainWindowOpen = false;
}
OnMainWindowClosed();
@ -187,7 +206,7 @@ namespace Artemis.UI.Screens
ChangeMaterialColors(ApplicationColorScheme.Light);
}
private void ApplyTryIconTheme(ThemeWatcher.WindowsTheme theme)
private void ApplyTrayIconTheme(ThemeWatcher.WindowsTheme theme)
{
Execute.PostToUIThread(() =>
{
@ -215,7 +234,7 @@ namespace Artemis.UI.Screens
private void _themeWatcher_SystemThemeChanged(object sender, WindowsThemeEventArgs e)
{
ApplyTryIconTheme(e.Theme);
ApplyTrayIconTheme(e.Theme);
}
private void ColorSchemeOnSettingChanged(object sender, EventArgs e)
@ -231,10 +250,7 @@ namespace Artemis.UI.Screens
public bool OpenMainWindow()
{
if (IsMainWindowOpen)
Execute.OnUIThread(FocusMainWindow);
else
TrayBringToForeground();
TrayBringToForeground();
return _rootViewModel.ScreenState == ScreenState.Active;
}
@ -250,13 +266,11 @@ namespace Artemis.UI.Screens
protected virtual void OnMainWindowOpened()
{
IsMainWindowOpen = true;
MainWindowOpened?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnMainWindowClosed()
{
IsMainWindowOpen = false;
MainWindowClosed?.Invoke(this, EventArgs.Empty);
}