diff --git a/VolumeControl.Core/Config.cs b/VolumeControl.Core/Config.cs index a656510ee..882a688b2 100644 --- a/VolumeControl.Core/Config.cs +++ b/VolumeControl.Core/Config.cs @@ -21,8 +21,11 @@ public class Config : AppConfig.ConfigurationFile /// /// Creates a new instance. /// - /// The first time this is called, the property is set to that instance; all subsequent calls do not update this property. - public Config(string filePath) : base(filePath) { } + /// The first time this is called, the property is set to that instance; all subsequent calls do not update this property. + public Config(string filePath) : base(filePath) + { + PropertyChanged += UpdateFLogState; + } #endregion Constructor #region Properties @@ -43,7 +46,7 @@ public Config(string filePath) : base(filePath) { } /// public void ResumeAutoSave() { - if (FLog.FilterEventType(Log.Enum.EventType.TRACE)) + if (FLog.FilterEventType(EventType.TRACE)) FLog.Trace($"Enabled {nameof(Config)} autosave."); _autoSaveEnabled = true; @@ -54,7 +57,7 @@ public void ResumeAutoSave() /// public void PauseAutoSave() { - if (FLog.FilterEventType(Log.Enum.EventType.TRACE)) + if (FLog.FilterEventType(EventType.TRACE)) FLog.Trace($"Disabled {nameof(Config)} autosave."); _autoSaveEnabled = false; @@ -85,7 +88,7 @@ public void AttachReflectivePropertyChangedHandlers() /// public override void Save(Formatting formatting = Formatting.Indented) { - if (FLog.FilterEventType(Log.Enum.EventType.TRACE)) + if (FLog.FilterEventType(EventType.TRACE)) FLog.Trace($"Saved {nameof(Config)}"); base.Save(formatting); } @@ -94,10 +97,31 @@ public override void Save(Formatting formatting = Formatting.Indented) #region EventHandlers private void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e) { - if (FLog.FilterEventType(Log.Enum.EventType.TRACE)) + if (FLog.FilterEventType(EventType.TRACE)) FLog.Trace($"Config property '{e.PropertyName}' was modified."); Save(); } + private void UpdateFLogState(object? sender, PropertyChangedEventArgs e) + { + if (!FLog.IsInitialized) return; + + // update the log's properties + if (e.PropertyName != null) + { + if (e.PropertyName.Equals(nameof(EnableLogging), StringComparison.Ordinal)) + { + FLog.Log.EndpointEnabled = EnableLogging; + } + else if (e.PropertyName.Equals(nameof(LogFilter), StringComparison.Ordinal)) + { + FLog.Log.EventTypeFilter = LogFilter; + } + else if (e.PropertyName.Equals(nameof(LogPath), StringComparison.Ordinal)) + { + FLog.ChangeLogPath(LogPath); + } + } + } private void PropertyWithPropertyChangedEvents_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (!_autoSaveEnabled) return; @@ -332,14 +356,6 @@ private void PropertyWithCollectionChangedEvents_CollectionChanged(object? sende /// public bool ShowPeakMeters { get; set; } = true; /// - /// The minimum boundary shown on peak meters. - /// - public const double PeakMeterMinValue = 0.0; - /// - /// The maximum boundary shown on peak meters. - /// - public const double PeakMeterMaxValue = 1.0; - /// /// Gets or sets whether volume & mute controls are visible in the Audio Devices list. /// public bool EnableDeviceControl { get; set; } = false; @@ -367,10 +383,10 @@ private void PropertyWithCollectionChangedEvents_CollectionChanged(object? sende // ^^^^^^^ // DO NOT RENAME THIS WITHOUT ALSO RENAMING IT IN VolumeControl.Log.SettingsInterface /// - /// Gets or sets the filter used for messages.
+ /// Gets or sets the filter used for messages.
/// See ///
- public Log.Enum.EventType LogFilter { get; set; } = Log.Enum.EventType.INFO | Log.Enum.EventType.WARN | Log.Enum.EventType.ERROR | Log.Enum.EventType.FATAL | Log.Enum.EventType.CRITICAL; + public EventType LogFilter { get; set; } = EventType.INFO | EventType.WARN | EventType.ERROR | EventType.FATAL | EventType.CRITICAL; // ^^^^^^^^^ // DO NOT RENAME THIS WITHOUT ALSO RENAMING IT IN VolumeControl.Log.SettingsInterface /// diff --git a/VolumeControl.Core/Input/Actions/HotkeyActionDefinition.cs b/VolumeControl.Core/Input/Actions/HotkeyActionDefinition.cs index c65fd769a..f7add49a6 100644 --- a/VolumeControl.Core/Input/Actions/HotkeyActionDefinition.cs +++ b/VolumeControl.Core/Input/Actions/HotkeyActionDefinition.cs @@ -135,7 +135,7 @@ private IActionSettingInstance[] CreateActionSettingInstances(IActionSettingInst } catch (Exception ex) { - if (FLog.Log.FilterEventType(Log.Enum.EventType.ERROR)) + if (FLog.Log.FilterEventType(EventType.ERROR)) FLog.Log.Error($"Failed to instantiate action setting \"{Name}\" due to an exception:", ex); } } diff --git a/VolumeControl.Core/Input/Actions/HotkeyActionInstance.cs b/VolumeControl.Core/Input/Actions/HotkeyActionInstance.cs index bd8c39624..fba2e401f 100644 --- a/VolumeControl.Core/Input/Actions/HotkeyActionInstance.cs +++ b/VolumeControl.Core/Input/Actions/HotkeyActionInstance.cs @@ -51,12 +51,12 @@ public void Invoke(object? sender, HotkeyPressedEventArgs e) try { HotkeyActionDefinition.Invoke_Unsafe(sender, e); - if (FLog.Log.FilterEventType(Log.Enum.EventType.TRACE)) + if (FLog.Log.FilterEventType(EventType.TRACE)) FLog.Log.Trace($"Successfully executed action \"{Name}\"."); } catch (Exception ex) { - if (FLog.Log.FilterEventType(Log.Enum.EventType.ERROR)) + if (FLog.Log.FilterEventType(EventType.ERROR)) FLog.Log.Error($"Action \"{Name}\" triggered an exception:", ex); } } diff --git a/VolumeControl.Core/Input/HotkeyActionAddonLoader.cs b/VolumeControl.Core/Input/HotkeyActionAddonLoader.cs index 787efa627..4c254abba 100644 --- a/VolumeControl.Core/Input/HotkeyActionAddonLoader.cs +++ b/VolumeControl.Core/Input/HotkeyActionAddonLoader.cs @@ -15,8 +15,9 @@ namespace VolumeControl.Core.Input /// public static class HotkeyActionAddonLoader { + #region LoadProviders /// - /// Loads DataTemplate providers from the specified and registers them with the + /// Loads DataTemplate providers from the specified and registers them with the . /// /// The instance to load the provider types into. /// Any number of instances that represent classes with the . @@ -36,11 +37,12 @@ public static void LoadProviders(ref TemplateProviderManager provider, params Ty } catch (Exception ex) { - FLog.Error($"[ActionLoader] Failed to load {nameof(DataTemplate)} provider type \"{type}\" due to an exception:", ex); + FLog.Error($"[ActionAddonLoader] Failed to load {nameof(DataTemplate)} provider type \"{type}\" due to an exception:", ex); } } } + #endregion LoadProviders #region ValidateMethodIsEligibleAsAction private enum EMethodValidationState : byte @@ -116,6 +118,7 @@ private static EMethodValidationState ValidateMethodIsEligibleAsAction(MethodInf } #endregion ValidateMethodIsEligibleAsAction + #region LoadActions /// /// Loads hotkey actions from the specified . /// @@ -145,7 +148,7 @@ public static HotkeyActionDefinition[] LoadActions(TemplateProviderManager provi // if this type doesn't have any public methods, skip it if (publicMethods.Length == 0) { - FLog.Error($"[ActionLoader] {type.FullName} doesn't contain any publicly-accessible methods marked with {typeof(HotkeyActionAttribute).FullName}!"); + FLog.Error($"[ActionAddonLoader] {type.FullName} doesn't contain any publicly-accessible methods marked with {typeof(HotkeyActionAttribute).FullName}!"); continue; } @@ -168,15 +171,15 @@ public static HotkeyActionDefinition[] LoadActions(TemplateProviderManager provi { // this doesn't need more information because ValidateMethodIsEligibleAsAction // logs all of the problems in detail anyway. - FLog.Error($"[ActionLoader] {method.GetFullMethodName()} was skipped because it is invalid."); + FLog.Error($"[ActionAddonLoader] {method.GetFullMethodName()} was skipped because it is invalid."); continue; } // get the action setting definitions for this method List actionSettingDefs = new(); - if (FLog.FilterEventType(Log.Enum.EventType.DEBUG)) - FLog.Debug($"[ActionLoader] Loading action setting definitions for \"{method.GetFullMethodName()}\""); + if (FLog.FilterEventType(EventType.DEBUG)) + FLog.Debug($"[ActionAddonLoader] Loading action setting definitions for \"{method.GetFullMethodName()}\""); foreach (var actionSettingAttribute in method.GetCustomAttributes()) { @@ -192,7 +195,7 @@ public static HotkeyActionDefinition[] LoadActions(TemplateProviderManager provi } catch (Exception ex) { - FLog.Error($"[ActionLoader] ", ex); + FLog.Error($"[ActionAddonLoader] ", ex); } if (dataTemplate == null) @@ -212,7 +215,7 @@ public static HotkeyActionDefinition[] LoadActions(TemplateProviderManager provi .GroupBy(d => d.Name) .Where(g => g.Count() > 1) .Select(g => $"\"{g.Key}\"")); - FLog.Error($"[ActionLoader] {method.GetFullMethodName()} was skipped because multiple settings have the same name: {duplicateNames}!"); + FLog.Error($"[ActionAddonLoader] {method.GetFullMethodName()} was skipped because multiple settings have the same name: {duplicateNames}!"); continue; } @@ -227,7 +230,7 @@ public static HotkeyActionDefinition[] LoadActions(TemplateProviderManager provi } catch (Exception ex) { - FLog.Error($"[ActionLoader] {method.GetFullMethodName()} was skipped because constructor of type {type.Name} threw an exception:", ex); + FLog.Error($"[ActionAddonLoader] {method.GetFullMethodName()} was skipped because constructor of type {type.Name} threw an exception:", ex); continue; } @@ -255,11 +258,11 @@ public static HotkeyActionDefinition[] LoadActions(TemplateProviderManager provi l.Add(hotkeyActionDefinition); ++loadedActionsFromTypeCount; - if (FLog.Log.FilterEventType(Log.Enum.EventType.TRACE)) + if (FLog.Log.FilterEventType(EventType.TRACE)) { List lines = new(); - var lineHeader = "[ActionLoader] "; + var lineHeader = "[ActionAddonLoader] "; lines.Add($"{lineHeader}Loaded {method.GetFullMethodName()}."); lineHeader = new string(' ', lineHeader.Length); for (int k = 0, k_max = actionSettingDefs.Count; k < k_max; ++k) @@ -273,14 +276,15 @@ public static HotkeyActionDefinition[] LoadActions(TemplateProviderManager provi } } //< enumerate public methods - if (FLog.Log.FilterEventType(Log.Enum.EventType.DEBUG)) - FLog.Log.Debug($"[ActionLoader] Loaded {loadedActionsFromTypeCount}{(loadedActionsFromTypeCount == methodsWithActionAttrCount ? "" : $"/{methodsWithActionAttrCount}")} actions from {type.FullName}"); + if (FLog.Log.FilterEventType(EventType.DEBUG)) + FLog.Log.Debug($"[ActionAddonLoader] Loaded {loadedActionsFromTypeCount}{(loadedActionsFromTypeCount == methodsWithActionAttrCount ? "" : $"/{methodsWithActionAttrCount}")} actions from {type.FullName}"); } //< enumerate public types - if (FLog.Log.FilterEventType(Log.Enum.EventType.DEBUG)) - FLog.Log.Debug($"[ActionLoader] Loaded {l.Count} total actions from {typesWithGroupAttrCount} action groups."); + if (FLog.Log.FilterEventType(EventType.DEBUG)) + FLog.Log.Debug($"[ActionAddonLoader] Loaded {l.Count} total actions from {typesWithGroupAttrCount} action groups."); return l.ToArray(); } + #endregion LoadActions } } diff --git a/VolumeControl.Core/Input/Json/HotkeyJsonObject.cs b/VolumeControl.Core/Input/Json/HotkeyJsonObject.cs index 9f344e9e0..9a01c2575 100644 --- a/VolumeControl.Core/Input/Json/HotkeyJsonObject.cs +++ b/VolumeControl.Core/Input/Json/HotkeyJsonObject.cs @@ -76,7 +76,7 @@ public THotkey CreateInstance(HotkeyActionManager actionManager, bool d } else { - if (FLog.FilterEventType(Log.Enum.EventType.ERROR)) + if (FLog.FilterEventType(EventType.ERROR)) FLog.Error($"Couldn't find an action with identifier \"{ActionIdentifier}\"!"); } } @@ -99,7 +99,7 @@ private static IActionSettingInstance[] ActionSettingsDictionaryToArray(Dictiona if (settingDefinition == null) { - if (FLog.FilterEventType(Log.Enum.EventType.WARN)) + if (FLog.FilterEventType(EventType.WARN)) FLog.Warning($"There is no action setting definition associated with JSON key '{name}' for action \"{actionDefinition.Identifier}\"."); continue; } @@ -112,7 +112,7 @@ private static IActionSettingInstance[] ActionSettingsDictionaryToArray(Dictiona } catch (Exception ex) { - if (FLog.FilterEventType(Log.Enum.EventType.ERROR)) + if (FLog.FilterEventType(EventType.ERROR)) FLog.Error($"An exception occurred while creating action setting \"{name}\" with value \"{value}\" for action \"{actionDefinition.Identifier}\":", ex); #if DEBUG throw; //< rethrow exception in DEBUG configuration @@ -123,7 +123,7 @@ private static IActionSettingInstance[] ActionSettingsDictionaryToArray(Dictiona if (settingInstance == null) { - if (FLog.FilterEventType(Log.Enum.EventType.ERROR)) + if (FLog.FilterEventType(EventType.ERROR)) FLog.Error($"An unknown error occurred while creating action setting \"{name}\" with value \"{value}\" for action \"{actionDefinition.Identifier}\"!"); continue; } diff --git a/VolumeControl.Core/Input/TemplateProviderManager.cs b/VolumeControl.Core/Input/TemplateProviderManager.cs index 1a8bea7ab..2d0e9cb5b 100644 --- a/VolumeControl.Core/Input/TemplateProviderManager.cs +++ b/VolumeControl.Core/Input/TemplateProviderManager.cs @@ -101,7 +101,7 @@ private bool TryCreateProvider(Type providerType, out ITemplateProvider provider _failedTypes.Add(providerType); return false; } - else if (FLog.FilterEventType(Log.Enum.EventType.TRACE)) + else if (FLog.FilterEventType(EventType.TRACE)) { FLog.Trace($"[{nameof(TemplateProviderManager)}] Successfully initialized {nameof(ITemplateProvider)} type \"{providerType}\"."); } @@ -135,7 +135,7 @@ private bool TryCreateDictionaryProvider(Type dictionaryProviderType, out ITempl _failedTypes.Add(dictionaryProviderType); return false; } - else if (FLog.FilterEventType(Log.Enum.EventType.TRACE)) + else if (FLog.FilterEventType(EventType.TRACE)) { FLog.Trace($"[{nameof(TemplateProviderManager)}] Successfully initialized {nameof(ITemplateDictionaryProvider)} type \"{dictionaryProviderType}\"."); } @@ -236,7 +236,7 @@ public bool TryGetDictionaryProvider(Type dictionaryProviderType, out ITemplateD if (dictionaryProvider.ProvideDataTemplate(key) is ActionSettingDataTemplate actionSettingDataTemplate) { // write trace log message - if (FLog.FilterEventType(Log.Enum.EventType.TRACE)) + if (FLog.FilterEventType(EventType.TRACE)) FLog.Trace($"[{nameof(TemplateProviderManager)}] Found DataTemplate with key \"{key}\" in {nameof(ITemplateDictionaryProvider)} type \"{dictionaryProvider.GetType()}\"."); return actionSettingDataTemplate; @@ -244,7 +244,7 @@ public bool TryGetDictionaryProvider(Type dictionaryProviderType, out ITemplateD } // write trace log message - if (FLog.FilterEventType(Log.Enum.EventType.TRACE)) + if (FLog.FilterEventType(EventType.TRACE)) FLog.Trace($"[{nameof(TemplateProviderManager)}] Couldn't find any DataTemplates with key \"{key}\"."); return null; @@ -266,7 +266,7 @@ public bool TryGetDictionaryProvider(Type dictionaryProviderType, out ITemplateD if (provider.ProvideDataTemplate(valueType) is ActionSettingDataTemplate actionSettingDataTemplate) { // write trace log message - if (FLog.FilterEventType(Log.Enum.EventType.TRACE)) + if (FLog.FilterEventType(EventType.TRACE)) FLog.Trace($"[{nameof(TemplateProviderManager)}] Found DataTemplate for value type \"{valueType}\" in {nameof(ITemplateProvider)} type \"{provider.GetType()}\"."); return actionSettingDataTemplate; @@ -281,7 +281,7 @@ public bool TryGetDictionaryProvider(Type dictionaryProviderType, out ITemplateD if (dictionaryProvider.ProvideDataTemplate(valueType) is ActionSettingDataTemplate actionSettingDataTemplate) { // write trace log message - if (FLog.FilterEventType(Log.Enum.EventType.TRACE)) + if (FLog.FilterEventType(EventType.TRACE)) FLog.Trace($"[{nameof(TemplateProviderManager)}] Found DataTemplate for value type \"{valueType}\" in {nameof(ITemplateDictionaryProvider)} type \"{dictionaryProvider.GetType()}\"."); return actionSettingDataTemplate; @@ -289,7 +289,7 @@ public bool TryGetDictionaryProvider(Type dictionaryProviderType, out ITemplateD } // write trace log message - if (FLog.FilterEventType(Log.Enum.EventType.TRACE)) + if (FLog.FilterEventType(EventType.TRACE)) FLog.Trace($"[{nameof(TemplateProviderManager)}] Couldn't find any DataTemplates for value type \"{valueType}\"."); return null; @@ -341,14 +341,14 @@ public enum FallbackMode /// was and the and didn't resolve to a valid template. public DataTemplate? FindDataTemplateFor(Type? providerType, string? templateKey, Type valueType, FallbackMode fallbackMode) { - bool traceMessagesAreEnabled = FLog.FilterEventType(Log.Enum.EventType.TRACE); + bool traceMessagesAreEnabled = FLog.FilterEventType(EventType.TRACE); // search specified provider for a data template if (providerType != null && !FailedTypes.Contains(providerType)) { if (providerType.IsAssignableTo(typeof(ITemplateProvider))) { // provider - if (templateKey != null && FLog.FilterEventType(Log.Enum.EventType.WARN)) + if (templateKey != null && FLog.FilterEventType(EventType.WARN)) { // [WARN] a key name was set, but the specified provider type doesn't support keys FLog.Warning( $"[{nameof(TemplateProviderManager)}] A {nameof(HotkeyActionSettingAttribute.DataTemplateProviderKey)} (\"{templateKey}\") was specified, but {nameof(HotkeyActionSettingAttribute.DataTemplateProviderType)} is {nameof(ITemplateProvider)} type \"{providerType}\"!", @@ -377,14 +377,14 @@ public enum FallbackMode } else { // [DEBUG] the specified provider does not support this value type - if (FLog.FilterEventType(Log.Enum.EventType.DEBUG)) + if (FLog.FilterEventType(EventType.DEBUG)) FLog.Debug($"[{nameof(TemplateProviderManager)}] {nameof(ITemplateProvider)} \"{providerType}\" does not support value type \"{valueType}\" ({StringHelper.GetFullMethodName(providerType.GetMethod(nameof(provider.CanProvideDataTemplate))!)} returned false)! Falling back to other providers."); } //< fallthrough } else if (!fallbackMode.HasFlag(FallbackMode.OnFailedProvider)) { // [ERROR] the specified provider type failed - if (FLog.FilterEventType(Log.Enum.EventType.ERROR)) + if (FLog.FilterEventType(EventType.ERROR)) FLog.Error($"[{nameof(TemplateProviderManager)}] {nameof(ITemplateProvider)} \"{providerType}\" failed! (see the failure message above for details)"); return null; @@ -410,7 +410,7 @@ public enum FallbackMode } else if (!fallbackMode.HasFlag(FallbackMode.OnFailedProvider)) { // [ERROR] the specified dictionary provider type failed - if (FLog.FilterEventType(Log.Enum.EventType.ERROR)) + if (FLog.FilterEventType(EventType.ERROR)) FLog.Error($"[{nameof(TemplateProviderManager)}] {nameof(ITemplateDictionaryProvider)} \"{providerType}\" failed! (see the failure message above for details)"); return null; @@ -446,14 +446,14 @@ public enum FallbackMode /// was and the and didn't resolve to a valid template. public DataTemplate? FindDataTemplateFor(Type? providerType, string? templateKey, Type valueType, bool allowFallbackOnMissingKey = false) { - bool showTraceMessages = FLog.FilterEventType(Log.Enum.EventType.TRACE); + bool showTraceMessages = FLog.FilterEventType(EventType.TRACE); // search specified provider for a data template if (providerType != null && !FailedTypes.Contains(providerType)) { if (providerType.IsAssignableTo(typeof(ITemplateProvider))) { // provider - if (templateKey != null && FLog.FilterEventType(Log.Enum.EventType.WARN)) + if (templateKey != null && FLog.FilterEventType(EventType.WARN)) { // [WARN] a key name was set, but the specified provider type doesn't support keys FLog.Warning( $"[{nameof(TemplateProviderManager)}] A {nameof(HotkeyActionSettingAttribute.DataTemplateProviderKey)} (\"{templateKey}\") was specified, but {nameof(HotkeyActionSettingAttribute.DataTemplateProviderType)} is {nameof(ITemplateProvider)} type \"{providerType}\"!", @@ -475,13 +475,13 @@ public enum FallbackMode } else { // [DEBUG] the specified provider does not support this value type - if (FLog.FilterEventType(Log.Enum.EventType.DEBUG)) + if (FLog.FilterEventType(EventType.DEBUG)) FLog.Debug($"[{nameof(TemplateProviderManager)}] {nameof(ITemplateProvider)} \"{providerType}\" does not support value type \"{valueType}\" ({StringHelper.GetFullMethodName(providerType.GetMethod(nameof(provider.CanProvideDataTemplate))!)} returned false)! Falling back to other providers."); } } else { // [ERROR] the specified provider type failed - if (FLog.FilterEventType(Log.Enum.EventType.ERROR)) + if (FLog.FilterEventType(EventType.ERROR)) FLog.Error($"[{nameof(TemplateProviderManager)}] {nameof(ITemplateProvider)} \"{providerType}\" failed! (see the failure message above for details)"); return null; @@ -507,7 +507,7 @@ public enum FallbackMode } else { // [ERROR] the specified dictionary provider type failed - if (FLog.FilterEventType(Log.Enum.EventType.ERROR)) + if (FLog.FilterEventType(EventType.ERROR)) FLog.Error($"[{nameof(TemplateProviderManager)}] {nameof(ITemplateDictionaryProvider)} \"{providerType}\" failed! (see the failure message above for details)"); return null; diff --git a/VolumeControl.CoreAudio/AudioDevice.cs b/VolumeControl.CoreAudio/AudioDevice.cs index e7ac2e223..9b494667a 100644 --- a/VolumeControl.CoreAudio/AudioDevice.cs +++ b/VolumeControl.CoreAudio/AudioDevice.cs @@ -33,7 +33,7 @@ internal AudioDevice(MMDevice mmDevice) AudioEndpointVolume.OnVolumeNotification += AudioEndpointVolume_OnVolumeNotification; - if (FLog.FilterEventType(Log.Enum.EventType.TRACE)) + if (FLog.FilterEventType(EventType.TRACE)) FLog.Trace($"[{nameof(AudioDevice)}] Successfully created {nameof(AudioDevice)} instance \"{FullName}\"."); } #endregion Constructor @@ -177,7 +177,7 @@ public float PeakMeterValue /// public void Dispose() { - if (FLog.Log.FilterEventType(Log.Enum.EventType.TRACE)) + if (FLog.Log.FilterEventType(EventType.TRACE)) FLog.Log.Trace($"Disposing of {nameof(AudioDevice)} instance \"{FullName}\""); ((IDisposable)this.MMDevice).Dispose(); GC.SuppressFinalize(this); diff --git a/VolumeControl.CoreAudio/AudioDeviceSessionManager.cs b/VolumeControl.CoreAudio/AudioDeviceSessionManager.cs index a1d6e9fba..0a6a113fa 100644 --- a/VolumeControl.CoreAudio/AudioDeviceSessionManager.cs +++ b/VolumeControl.CoreAudio/AudioDeviceSessionManager.cs @@ -28,7 +28,7 @@ internal AudioDeviceSessionManager(AudioDevice audioDevice) AudioSessionManager2.OnSessionCreated += this.AudioSessionManager_OnSessionCreated; - bool showTraceLogMessages = FLog.FilterEventType(Log.Enum.EventType.TRACE); + bool showTraceLogMessages = FLog.FilterEventType(EventType.TRACE); if (AudioSessionManager2.Sessions is not null) { // populate the sessions list diff --git a/VolumeControl.CoreAudio/AudioSession.cs b/VolumeControl.CoreAudio/AudioSession.cs index f0d216769..ef9af49d0 100644 --- a/VolumeControl.CoreAudio/AudioSession.cs +++ b/VolumeControl.CoreAudio/AudioSession.cs @@ -18,7 +18,7 @@ public class AudioSession : IAudioControl, IReadOnlyAudioControl, IHideableAudio #region Constructor internal AudioSession(AudioDevice owningDevice, AudioSessionControl2 audioSessionControl2) { - bool showTraceLogMessages = FLog.FilterEventType(Log.Enum.EventType.TRACE); + bool showTraceLogMessages = FLog.FilterEventType(EventType.TRACE); AudioDevice = owningDevice; AudioSessionControl = audioSessionControl2; @@ -354,7 +354,7 @@ public bool HasMatchingName(string name, StringComparison stringComparison = Str /// public void Dispose() { - if (FLog.Log.FilterEventType(Log.Enum.EventType.TRACE)) + if (FLog.Log.FilterEventType(EventType.TRACE)) FLog.Log.Trace($"Disposing of {nameof(AudioSession)} instance \"{ProcessIdentifier}\""); this.AudioSessionControl.Dispose(); GC.SuppressFinalize(this); diff --git a/VolumeControl.Log/AsyncLogWriter.cs b/VolumeControl.Log/AsyncLogWriter.cs index 77528e8ee..c84f53f8b 100644 --- a/VolumeControl.Log/AsyncLogWriter.cs +++ b/VolumeControl.Log/AsyncLogWriter.cs @@ -1,15 +1,17 @@ using System.Collections; +using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; -using VolumeControl.Log.Endpoints; -using VolumeControl.Log.Enum; +using VolumeControl.Log.Helpers; +using VolumeControl.Log.Interfaces; namespace VolumeControl.Log { /// /// Asynchonously writes messages to the log endpoint. /// - public sealed class AsyncLogWriter : ThreadedLogger, ILogWriter, IDisposable + public sealed class AsyncLogWriter : ThreadedActionQueue, ILogWriter, INotifyPropertyChanged, IDisposable { #region Constructor /// @@ -17,10 +19,10 @@ public sealed class AsyncLogWriter : ThreadedLogger, ILogWriter, IDisposable /// /// A log endpoint instance. /// The default event type filter. - public AsyncLogWriter(IEndpoint endpoint, EventType eventTypeFilter) : base() + public AsyncLogWriter(IEndpointWriter endpoint, EventType eventTypeFilter) : base() { Endpoint = endpoint; - EventTypeFilter = eventTypeFilter; + _eventTypeFilter = eventTypeFilter; } static AsyncLogWriter() { @@ -31,7 +33,7 @@ static AsyncLogWriter() #endregion Constructor #region Fields - internal readonly IEndpoint Endpoint; + internal readonly IEndpointWriter Endpoint; /// /// The string that defines the format of timestamps. /// @@ -70,21 +72,31 @@ public bool EndpointEnabled { lock (Endpoint) { - return Endpoint.Enabled; + return Endpoint.IsWritingEnabled; } } set { lock (Endpoint) { - Endpoint.Enabled = value; + Endpoint.IsWritingEnabled = value; } + NotifyPropertyChanged(); } } /// /// Gets or sets the event type filter that determines which message types are visible for this log writer instance. /// - public EventType EventTypeFilter { get; set; } + public EventType EventTypeFilter + { + get => _eventTypeFilter; + set + { + _eventTypeFilter = value; + NotifyPropertyChanged(); + } + } + private EventType _eventTypeFilter; /// /// Gets or sets whether log messages are added to the queue to be written asynchronously, or written synchronously. /// @@ -94,7 +106,7 @@ public bool IsAsyncEnabled get => _isAsyncEnabled; set { - if (_isAsyncEnabled == value) return; + if (value == _isAsyncEnabled) return; if (value) { // enable async @@ -105,14 +117,20 @@ public bool IsAsyncEnabled _isAsyncEnabled = false; Flush(); } + NotifyPropertyChanged(); } } private bool _isAsyncEnabled = true; #endregion Properties - #region WriteLogMessage Override + #region Events /// - protected override void WriteLogMessage(LogMessage message) + public event PropertyChangedEventHandler? PropertyChanged; + private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new(propertyName)); + #endregion Events + + #region (Private) WriteLogMessage + private void WriteLogMessage(LogMessage message) { if (!FilterEventType(message.EventType)) { // event type is not enabled; don't show it @@ -121,9 +139,9 @@ protected override void WriteLogMessage(LogMessage message) lock (Endpoint) { - if (!Endpoint.Enabled) return; + if (!Endpoint.IsWritingEnabled) return; - using var writer = Endpoint.GetWriter(); + using var writer = Endpoint.GetTextWriter(); if (writer == null) return; @@ -166,7 +184,7 @@ protected override void WriteLogMessage(LogMessage message) writer.Flush(); } } - #endregion WriteLogMessage Override + #endregion (Private) WriteLogMessage #region Methods @@ -239,7 +257,7 @@ public bool FilterEventType(EventType eventType) #region ResetEndpoint /// - /// Resets the endpoint to its default state by calling . + /// Resets the endpoint to its default state by calling . /// public void ResetEndpoint() { @@ -249,7 +267,7 @@ public void ResetEndpoint() } } /// - /// Resets the endpoint to its default state by calling , and writes the specified . + /// Resets the endpoint to its default state by calling , and writes the specified . /// /// The first line to (synchronously) write to the log. public void ResetEndpoint(string firstLine) @@ -257,7 +275,7 @@ public void ResetEndpoint(string firstLine) lock (Endpoint) { Endpoint.Reset(); - Endpoint.WriteRawLine(firstLine); + Endpoint.WriteLine(firstLine); } } #endregion ResetEndpoint @@ -267,14 +285,15 @@ public void ResetEndpoint(string firstLine) /// Writes the specified to the log. /// /// - /// When IsAsyncEnabled is , the message is added to the queue and written asynchronously; otherwise, the message is blocking and the message is written synchronously. + /// When IsAsyncEnabled is , the message is written asynchronously; + /// otherwise, the message is written synchronously and the caller will be blocked until the message has been written. /// - /// - public new void LogMessage(LogMessage logMessage) + /// The message to write to the log. + public void LogMessage(LogMessage logMessage) { if (IsAsyncEnabled) { - base.LogMessage(logMessage); + Enqueue(() => WriteLogMessage(logMessage)); } else // async is disabled { @@ -333,9 +352,13 @@ public void ResetEndpoint(string firstLine) /// /// Sets the IsAsyncEnabled property to without flushing the queue. /// + /// + /// Queued messages will still be written, but any further messages will be written synchronously. + /// public void DisableAsyncNoFlush() { _isAsyncEnabled = false; + NotifyPropertyChanged(nameof(IsAsyncEnabled)); } #endregion DisableAsyncNoFlush diff --git a/VolumeControl.Log/Endpoints/BaseEndpointWriter.cs b/VolumeControl.Log/Endpoints/BaseEndpointWriter.cs new file mode 100644 index 000000000..96fe8ed9c --- /dev/null +++ b/VolumeControl.Log/Endpoints/BaseEndpointWriter.cs @@ -0,0 +1,86 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using VolumeControl.Log.Interfaces; + +namespace VolumeControl.Log.Endpoints +{ + /// + /// base class for endpoint writers. + /// + public abstract class BaseEndpointWriter : IEndpointWriter, INotifyPropertyChanged + { + #region Constructor + /// + /// Creates a new instance. + /// + /// The initial state + protected BaseEndpointWriter(bool isWritingEnabled = true) + { + _isWritingEnabled = isWritingEnabled; + } + #endregion Constructor + + #region Properties + /// + public bool IsWritingEnabled + { + get => _isWritingEnabled; + set + { + NotifyEnabledChanging(value); + _isWritingEnabled = value; + NotifyEnabledChanged(_isWritingEnabled); + } + } + private bool _isWritingEnabled; + #endregion Properties + + #region Events + /// + public event EventHandler? EnabledChanging; + private void NotifyEnabledChanging(bool incomingState) => EnabledChanging?.Invoke(this, incomingState); + /// + public event EventHandler? EnabledChanged; + private void NotifyEnabledChanged(bool newState) => EnabledChanged?.Invoke(this, newState); + /// + public event PropertyChangedEventHandler? PropertyChanged; + /// + /// Triggers the PropertyChanged event for the specified . + /// + /// The name of the property that was changed. + protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new(propertyName)); + #endregion Events + + #region (Abstract) Methods + /// + public abstract TextWriter GetTextWriter(); + /// + public abstract void Reset(); + #endregion (Abstract) Methods + + #region Methods + /// + public virtual void Write(string @string) + { + if (!IsWritingEnabled) return; + + using var writer = GetTextWriter(); + writer.Write(@string); + writer.Flush(); + } + /// + public virtual void WriteLine(string @string) + { + if (!IsWritingEnabled) return; + + using var writer = GetTextWriter(); + writer.WriteLine(@string); + writer.Flush(); + } + /// + /// Writes a line break to the endpoint. + /// + public virtual void WriteLine() => WriteLine(string.Empty); + #endregion Methods + } +} \ No newline at end of file diff --git a/VolumeControl.Log/Endpoints/ConsoleEndpoint.cs b/VolumeControl.Log/Endpoints/ConsoleEndpoint.cs index bec93cf83..84a2b7fff 100644 --- a/VolumeControl.Log/Endpoints/ConsoleEndpoint.cs +++ b/VolumeControl.Log/Endpoints/ConsoleEndpoint.cs @@ -3,52 +3,38 @@ /// /// Allows using the as a logging endpoint. /// - public class ConsoleEndpoint : IEndpoint + public class ConsoleEndpoint : BaseEndpointWriter { #region Constructor - /// - public ConsoleEndpoint() => this.Enabled = true; + /// + /// Creates a new instance. + /// + public ConsoleEndpoint() : base(true) { } #endregion Constructor - #region Properties - /// - public bool Enabled - { - get => _enabled; - set - { - NotifyEnabledChanging(value); - _enabled = value; - NotifyEnabledChanged(_enabled); - } - } - private bool _enabled; - #endregion Properties - - #region Events - /// - public event EventHandler? EnabledChanging; - private void NotifyEnabledChanging(bool incomingState) => EnabledChanging?.Invoke(this, incomingState); - /// - public event EventHandler? EnabledChanged; - private void NotifyEnabledChanged(bool newState) => EnabledChanged?.Invoke(this, newState); - #endregion Events - #region Methods - /// - public TextReader? GetReader() => Console.In; - /// - public TextWriter? GetWriter() => Console.Out; - /// - public int? ReadRaw() => Console.Read(); - /// - public string? ReadRawLine() => Console.ReadLine(); - /// - public void Reset() => Console.Clear(); - /// - public void WriteRaw(string? str) => Console.Write(str); - /// - public void WriteRawLine(string? str = null) => Console.WriteLine(str); + /// + /// Gets the instance for the standard output console stream. + /// + /// + /// The caller should not dispose of the returned writer object. + /// + /// instance. + public static TextWriter GetSTDOUT() => Console.Out; + /// + /// Gets the instance for the standard error console stream. + /// + /// + /// The caller should not dispose of the returned writer object. + /// + /// instance. + public static TextWriter GetSTDERR() => Console.Error; + /// + public override TextWriter GetTextWriter() => Console.Out; + /// + /// Clears the console. + /// + public override void Reset() => Console.Clear(); #endregion Methods } } diff --git a/VolumeControl.Log/Endpoints/FileEndpoint.cs b/VolumeControl.Log/Endpoints/FileEndpoint.cs index b8f242706..dbc553215 100644 --- a/VolumeControl.Log/Endpoints/FileEndpoint.cs +++ b/VolumeControl.Log/Endpoints/FileEndpoint.cs @@ -1,112 +1,61 @@ -namespace VolumeControl.Log.Endpoints +using VolumeControl.Log.Interfaces; + +namespace VolumeControl.Log.Endpoints { /// /// Log endpoint that allows writing logs directly to a file on disk. /// - public class FileEndpoint : IEndpoint + public class FileEndpoint : BaseEndpointWriter, IEndpointWriter { #region Constructors /// /// The location of the log file. /// Whether the endpoint is already enabled when constructed or not. - public FileEndpoint(string path, bool enabled) + public FileEndpoint(string path, bool enabled) : base(enabled) { - this.Path = path; - this.Enabled = enabled; + _path = path; } #endregion Constructors #region Properties - /// - public bool Enabled + /// + /// The location of the log file. + /// + public string Path { - get => _enabled; + get => _path; set { - NotifyEnabledChanging(value); - _enabled = value; - NotifyEnabledChanged(_enabled); + _path = value; + NotifyPropertyChanged(); } } - private bool _enabled; + private string _path; /// - /// The location of the log file. + /// Gets whether the Path is a blank string or not. /// - public string Path { get; set; } + /// when the Path is a blank string; otherwise . + public bool PathIsEmpty => Path.Length == 0; #endregion Properties - #region Events - /// - public event EventHandler? EnabledChanging; - private void NotifyEnabledChanging(bool incomingState) => EnabledChanging?.Invoke(this, incomingState); - /// - public event EventHandler? EnabledChanged; - private void NotifyEnabledChanged(bool newState) => EnabledChanged?.Invoke(this, newState); - #endregion Events - #region Methods - internal TextReader? GetReader(FileStreamOptions open) => !this.Enabled || this.Path.Length == 0 ? null : (TextReader)new StreamReader(File.Open(this.Path, open)); - internal TextWriter? GetWriter(FileStreamOptions open) => !this.Enabled || this.Path.Length == 0 ? null : (TextWriter)new StreamWriter(File.Open(this.Path, open)); - /// - public TextReader? GetReader() => !this.Enabled || this.Path.Length == 0 - ? null - : this.GetReader(new() { Mode = FileMode.Open, Access = FileAccess.Read, Share = FileShare.ReadWrite }); - /// - public TextWriter? GetWriter() => !this.Enabled || this.Path.Length == 0 - ? null - : this.GetWriter(new() { Mode = FileMode.Append, Access = FileAccess.Write, Share = FileShare.ReadWrite }); - - /// - public void WriteRaw(string? str, FileMode mode) - { - if (!this.Enabled || this.Path.Length == 0) - return; - using StreamWriter w = new(File.Open(this.Path, mode, FileAccess.Write, FileShare.Read)) { AutoFlush = true }; - w.Write(str); - w.Flush(); - } - /// - public void WriteRaw(string? str) => this.WriteRaw(str, FileMode.Append); - /// - public void WriteRawLine(string? str, FileMode mode) - { - if (!this.Enabled || this.Path.Length == 0) - return; - using StreamWriter w = new(File.Open(this.Path, mode, FileAccess.Write, FileShare.Read)) { AutoFlush = true }; - w.WriteLine(str); - w.Flush(); - } - /// - public void WriteRawLine(string? str) => this.WriteRawLine(str, FileMode.Append); - /// - public int? ReadRaw(FileMode mode) - { - if (!this.Enabled || this.Path.Length == 0) - return null; - using StreamReader r = new(File.Open(this.Path, mode, FileAccess.Read, FileShare.ReadWrite)); - return r.Read(); - } + internal StreamWriter? GetWriter(FileMode fileMode, FileAccess fileAccess, FileShare fileShare) => !this.IsWritingEnabled || this.Path.Length == 0 ? null : new StreamWriter(File.Open(this.Path, fileMode, fileAccess, fileShare)); + /// + /// Gets a new instance for the file. The caller is responsible for disposing of it. + /// + /// A new instance when this endpoint is enabled; otherwise . + public StreamWriter GetStreamWriter() => !this.IsWritingEnabled || this.Path.Length == 0 + ? null! + : this.GetWriter(FileMode.Append, FileAccess.Write, FileShare.Write)!; /// - public string? ReadRawLine(FileMode mode) - { - if (!this.Enabled || this.Path.Length == 0) - return null; - using StreamReader r = new(File.Open(this.Path, mode, FileAccess.Read, FileShare.ReadWrite)); - return r.ReadLine(); - } + public override TextWriter GetTextWriter() => GetStreamWriter(); /// - public void Reset() + public override void Reset() { - if (!this.Enabled || !File.Exists(this.Path)) + if (!this.IsWritingEnabled || !File.Exists(this.Path)) return; File.Open(this.Path, FileMode.Truncate, FileAccess.Write, FileShare.ReadWrite).Dispose(); } - - /// - public int? ReadRaw() => this.ReadRaw(FileMode.Open); - /// - public string? ReadRawLine() => this.ReadRawLine(FileMode.Open); - #endregion Methods } } \ No newline at end of file diff --git a/VolumeControl.Log/Endpoints/IEndpoint.cs b/VolumeControl.Log/Endpoints/IEndpoint.cs deleted file mode 100644 index 4c2643a40..000000000 --- a/VolumeControl.Log/Endpoints/IEndpoint.cs +++ /dev/null @@ -1,79 +0,0 @@ -namespace VolumeControl.Log.Endpoints -{ - /// - /// Represents an endpoint output target for a log writer, and exposes helper methods for interacting with it. - /// - public interface IEndpoint - { - #region Properties - /// - /// Return true when the endpoint is enabled. - /// - bool Enabled { get; set; } - #endregion Properties - - #region Events - /// - /// Occurs when the endpoint is about to be enabled or disabled for any reason. - /// - /// - /// The boolean argument is the incoming state. - /// - event EventHandler? EnabledChanging; - /// - /// Occurs when the endpoint is enabled or disabled for any reason. - /// - /// - /// The boolean argument is the new state. - /// - event EventHandler? EnabledChanged; - #endregion Events - - #region Methods - /// - /// Retrieve a object for reading from the endpoint.

- /// Using this is only recommended for repeated read operations, such as in a loop; There is no benefit to using this for single read operations. - ///
- /// - /// nullThe endpoint isn't enabled. - /// A reader using this endpoint's output target. - /// - TextReader? GetReader(); - /// - /// Retrieve a object for writing to the endpoint.

- /// Using this is only recommended for repeated write operations, such as in a loop; There is no benefit to using this for single write operations. - ///
- /// - /// nullThe endpoint isn't enabled. - /// A writer using this endpoint's output target. - /// - TextWriter? GetWriter(); - /// - /// Write to the filestream. - /// It is highly recommended that you do not use this function, as it doesn't conform to formatting rules. - /// - /// A string to write. - void WriteRaw(string? str); - /// - /// Write to the filestream, and append a newline. - /// It is highly recommended that you do not use this function, as it doesn't conform to formatting rules. - /// - /// A string to write. - void WriteRawLine(string? str = null); - /// - /// Read a character from the log endpoint. - /// - /// Integer (4-byte) representation of a single character. - int? ReadRaw(); - /// - /// Read a line from the log endpoint. - /// - /// A string containing one line from the log endpoint. - string? ReadRawLine(); - /// - /// Reset the contents of the log endpoint, leaving it empty. - /// - void Reset(); - #endregion Methods - } -} \ No newline at end of file diff --git a/VolumeControl.Log/Endpoints/MemoryEndpoint.cs b/VolumeControl.Log/Endpoints/MemoryEndpoint.cs index 67dfb6699..273ef34c1 100644 --- a/VolumeControl.Log/Endpoints/MemoryEndpoint.cs +++ b/VolumeControl.Log/Endpoints/MemoryEndpoint.cs @@ -1,9 +1,11 @@ -namespace VolumeControl.Log.Endpoints +using VolumeControl.Log.Interfaces; + +namespace VolumeControl.Log.Endpoints { /// - /// A log endpoint that implements and uses a as an endpoint. + /// A log endpoint that implements and uses a as an endpoint. /// - public class MemoryEndpoint : IEndpoint, IDisposable + public class MemoryEndpoint : IEndpointWriter, IDisposable { #region Constructor /// @@ -12,7 +14,7 @@ public class MemoryEndpoint : IEndpoint, IDisposable public MemoryEndpoint(bool enabled = true, int kilobytes = 10) { _stream = new(new byte[1024 * kilobytes], true); - this.Enabled = enabled; + this.IsWritingEnabled = enabled; } #endregion Constructor @@ -22,7 +24,7 @@ public MemoryEndpoint(bool enabled = true, int kilobytes = 10) #region Properties /// - public bool Enabled + public bool IsWritingEnabled { get => _enabled; set @@ -45,25 +47,31 @@ public bool Enabled #endregion Events #region Methods + /// + /// Gets a new instance for this memory endpoint. + /// + /// + /// The caller is responsible for disposing of the returned reader object. + /// + /// New instance. + public TextReader GetTextReader() => this.IsWritingEnabled ? new StreamReader(_stream, leaveOpen: true) : null!; /// - public TextReader? GetReader() => this.Enabled ? new StreamReader(_stream, leaveOpen: true) : null; - /// - public TextWriter? GetWriter() => this.Enabled ? new StreamWriter(_stream, leaveOpen: true) : null; + public TextWriter GetTextWriter() => this.IsWritingEnabled ? new StreamWriter(_stream, leaveOpen: true) : null!; /// public int? ReadRaw() { - if (!this.Enabled) + if (!this.IsWritingEnabled) return null; - using TextReader? r = this.GetReader(); + using TextReader? r = this.GetTextReader(); int? ch = r?.Read(); return ch; } /// public string? ReadRawLine() { - if (!this.Enabled) + if (!this.IsWritingEnabled) return null; - using TextReader? r = this.GetReader(); + using TextReader? r = this.GetTextReader(); string? line = r?.ReadLine(); r?.Dispose(); return line; @@ -71,18 +79,18 @@ public bool Enabled /// public void Reset() => _stream = new(); /// - public void WriteRaw(string? str) + public void Write(string? str) { - if (!this.Enabled || str == null) + if (!this.IsWritingEnabled || str == null) return; _stream.Write(new Span(str.ToCharArray().Cast().ToArray())); } /// - public void WriteRawLine(string? str = null) + public void WriteLine(string? str = null) { - if (!this.Enabled) + if (!this.IsWritingEnabled) return; - this.WriteRaw(str == null ? "\n" : $"{str}\n"); + this.Write(str == null ? "\n" : $"{str}\n"); } /// diff --git a/VolumeControl.Log/Enum/EventType.cs b/VolumeControl.Log/Enum/EventType.cs deleted file mode 100644 index f016f2ee7..000000000 --- a/VolumeControl.Log/Enum/EventType.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace VolumeControl.Log.Enum -{ - /// - /// Determines the header used to print formatted log messages. - /// - [Flags] - public enum EventType : byte - { - /// - /// No event types. This is the 0 value for the EventType flagset. - /// This EventType does not have an associated header, and may only be used for type filtering. - /// If used anyway, it produces this header: "[????]" - /// - NONE = 0, - /// - /// A debugging message that should only be shown when in debug mode. - /// Produces header: "[DEBUG]" - /// - DEBUG = 1, - /// - /// An informational message. - /// Produces header: "[INFO]" - /// - INFO = 2, - /// - /// A warning message. - /// Produces header: "[WARN]" - /// - WARN = 4, - /// - /// An error message. - /// Produces header: "[ERROR]" - /// - ERROR = 8, - /// - /// A fatal error message. - /// Produces header: "[FATAL]" - /// - FATAL = 16, - /// - /// A critical message that does not necessarily indicate failure, but cannot be prevented from appearing in the log by user settings. - /// Produces header: "[CRITICAL]" - /// - CRITICAL = 32, - /// - /// Extremely situational debug information. - /// Produces header: "[TRACE]" - /// - TRACE = 64, - } -} diff --git a/VolumeControl.Log/EventType.cs b/VolumeControl.Log/EventType.cs new file mode 100644 index 000000000..8515728c6 --- /dev/null +++ b/VolumeControl.Log/EventType.cs @@ -0,0 +1,58 @@ +namespace VolumeControl.Log +{ + /// + /// Determines the header used to print formatted log messages. + /// + [Flags] + public enum EventType : byte + { + /// + /// Not an event type. + /// + NONE = 0, + /// + /// Message contains debugging information. + /// + /// + /// For debug info that is only useful in certain situations, see . + /// + DEBUG = 1, + /// + /// Message contains information that isn't related to an error or warning. + /// + INFO = 2, + /// + /// Messages related to a warning or very minor error. + /// + WARN = 4, + /// + /// Messages related to an error. + /// + ERROR = 8, + /// + /// Messages related to a significant error of critical importance. + /// + /// + /// For errors that the application cannot recover from, see . + /// + CRITICAL = 16, + /// + /// Messages related to a significant error that the application cannot recover from. + /// + /// + /// For significant errors that did not cause the application to exit unexpectedly, see . + /// + FATAL = 32, + /// + /// Message contains debugging information that is only situationally useful. + /// + /// + /// For debug info that is generally useful in most or all case, see . + /// + TRACE = 64, + /// + /// All event types. + /// + ALL = DEBUG | INFO | WARN | ERROR | CRITICAL | FATAL | TRACE, + } +} diff --git a/VolumeControl.Log/NotInitializedException.cs b/VolumeControl.Log/Exceptions/NotInitializedException.cs similarity index 95% rename from VolumeControl.Log/NotInitializedException.cs rename to VolumeControl.Log/Exceptions/NotInitializedException.cs index 63bd3d034..550830108 100644 --- a/VolumeControl.Log/NotInitializedException.cs +++ b/VolumeControl.Log/Exceptions/NotInitializedException.cs @@ -1,4 +1,4 @@ -namespace VolumeControl.Log +namespace VolumeControl.Log.Exceptions { /// /// Represents errors that occur as a result of trying to access an object before it is initialized. diff --git a/VolumeControl.Log/FLog.cs b/VolumeControl.Log/FLog.cs index cafd84b3f..2d484676b 100644 --- a/VolumeControl.Log/FLog.cs +++ b/VolumeControl.Log/FLog.cs @@ -1,13 +1,13 @@ using VolumeControl.Log.Endpoints; -using VolumeControl.Log.Enum; +using VolumeControl.Log.Exceptions; namespace VolumeControl.Log { /// - /// Static logger class. + /// Static file logger class. /// /// - /// The method must be called before accessing any properties, and after the settings have initialized. + /// The method must be called before accessing any properties. /// public static class FLog { @@ -17,7 +17,6 @@ public static class FLog #endregion Fields #region Properties - private static SettingsInterface SettingsInterface => SettingsInterface.Default; /// /// Gets the instance. /// @@ -49,6 +48,13 @@ public static bool IsAsyncEnabled _logWriter.IsAsyncEnabled = value; } } + /// + /// Gets whether this log has been initialized yet or not. + /// + /// + /// This does not throw exceptions. + /// + public static bool IsInitialized => _initialized; #endregion Properties #region Methods @@ -59,12 +65,12 @@ public static bool IsAsyncEnabled /// /// Displayed in the header as "Log (verb)" private static string MakeInitMessage(string verb) - => $"{AsyncLogWriter.DateTimeFormatString}{AsyncLogWriter.Indent(AsyncLogWriter.TimestampLength, AsyncLogWriter.DateTimeFormatString.Length)}{new string(' ', AsyncLogWriter.EventTypeLength)}=== Log {verb} @ {DateTime.UtcNow:U} === {{ {nameof(SettingsInterface.LogFilter)}: {(int)Log.EventTypeFilter} ({Log.EventTypeFilter:G}) }}{Environment.NewLine}"; + => $"{AsyncLogWriter.DateTimeFormatString}{AsyncLogWriter.Indent(AsyncLogWriter.TimestampLength, AsyncLogWriter.DateTimeFormatString.Length)}{new string(' ', AsyncLogWriter.EventTypeLength)}=== Log {verb} @ {DateTime.UtcNow:U} === {{ LogFilter: {(int)Log.EventTypeFilter} ({Log.EventTypeFilter:G}) }}{Environment.NewLine}"; private static void WriteRaw(string text) { lock (Log.Endpoint) { - Log.Endpoint.WriteRaw(text); + Log.Endpoint.Write(text); } } private static NotInitializedException MakeLogNotInitializedException(Exception? innerException = null) @@ -77,21 +83,16 @@ private static NotInitializedException MakeLogNotInitializedException(Exception? /// /// The method has already been called before. /// The default config object hasn't been initialized yet. - public static void Initialize() + public static void Initialize(string path, bool enable, EventType logFilter, bool deleteExisting = true) { if (_initialized) // already initialized throw new InvalidOperationException($"{nameof(FLog)} is already initialized! {nameof(Initialize)}() can only be called once."); - else if (SettingsInterface == null) // settings aren't initialized yet - throw new NotInitializedException(nameof(SettingsInterface), $"The default {nameof(AppConfig.Configuration)} instance must be initialized prior to calling {nameof(Initialize)}()!"); - - // attach property changed handler to the settings object so we can react to changes - SettingsInterface.PropertyChanged += SettingsInterface_PropertyChanged; - _logWriter = new(new FileEndpoint(SettingsInterface.LogPath, SettingsInterface.EnableLogging), SettingsInterface.LogFilter); + _logWriter = new(new FileEndpoint(path, enable), logFilter); _initialized = true; //< Log is valid here - if (SettingsInterface.LogClearOnInitialize) + if (deleteExisting) { Log.ResetEndpoint(MakeInitMessage("Initialized")); } @@ -99,6 +100,20 @@ public static void Initialize() } #endregion Initialize + #region ChangeLogPath + /// + /// Sets the log path to a new location. + /// + /// The filepath to change the log path to. + public static void ChangeLogPath(string newPath) + { + if (!_initialized) + throw MakeLogNotInitializedException(); + + ((FileEndpoint)Log.Endpoint).Path = newPath; + } + #endregion ChangeLogPath + #region AsyncLogWriter Methods /// public static bool FilterEventType(EventType eventType) => Log.FilterEventType(eventType); @@ -123,34 +138,5 @@ public static void Initialize() #endregion AsyncLogWriter Methods #endregion Methods - - #region EventHandlers - - #region SettingsInterface - private static void SettingsInterface_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) - { - if (e.PropertyName == null || !_initialized) return; - - if (e.PropertyName.Equals(nameof(SettingsInterface.EnableLogging), StringComparison.Ordinal)) - { - Log.EndpointEnabled = SettingsInterface.EnableLogging; - } - else if (e.PropertyName.Equals(nameof(SettingsInterface.LogPath), StringComparison.Ordinal)) - { - lock (Log.Endpoint) - { - ((FileEndpoint)Log.Endpoint).Path = SettingsInterface.LogPath; - } - } - else if (e.PropertyName.Equals(nameof(SettingsInterface.LogFilter), StringComparison.Ordinal)) - { - Log.EventTypeFilter = SettingsInterface.LogFilter; - - WriteRaw(MakeInitMessage("Filter Changed")); - } - } - #endregion SettingsInterface - - #endregion EventHandlers } } diff --git a/VolumeControl.Log/FodyWeavers.xml b/VolumeControl.Log/FodyWeavers.xml deleted file mode 100644 index 4e68ed1a8..000000000 --- a/VolumeControl.Log/FodyWeavers.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/VolumeControl.Log/FodyWeavers.xsd b/VolumeControl.Log/FodyWeavers.xsd deleted file mode 100644 index 69dbe488c..000000000 --- a/VolumeControl.Log/FodyWeavers.xsd +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - Used to control if the On_PropertyName_Changed feature is enabled. - - - - - Used to control if the Dependent properties feature is enabled. - - - - - Used to control if the IsChanged property feature is enabled. - - - - - Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form. - - - - - Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project. - - - - - Used to control if equality checks should use the Equals method resolved from the base class. - - - - - Used to control if equality checks should use the static Equals method resolved from the base class. - - - - - Used to turn off build warnings from this weaver. - - - - - Used to turn off build warnings about mismatched On_PropertyName_Changed methods. - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/VolumeControl.Log/ExceptionMessageHelper.cs b/VolumeControl.Log/Helpers/ExceptionMessageHelper.cs similarity index 98% rename from VolumeControl.Log/ExceptionMessageHelper.cs rename to VolumeControl.Log/Helpers/ExceptionMessageHelper.cs index bdc3881a6..1e7bc91d6 100644 --- a/VolumeControl.Log/ExceptionMessageHelper.cs +++ b/VolumeControl.Log/Helpers/ExceptionMessageHelper.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; using System.Text; -namespace VolumeControl.Log +namespace VolumeControl.Log.Helpers { /// /// Helper methods for converting exceptions into nicely formatted strings. @@ -144,7 +144,7 @@ public static string MakeExceptionMessage(Exception exception, string linePrefix var value = propInfo.GetValue(exception); // skip properties with null/empty values - if (value == null || (value is string s && s.Length == 0)) + if (value == null || value is string s && s.Length == 0) continue; sb.Append($"{tabPrefix}\"{propInfo.Name}\": \"{value}\"{endline}"); @@ -155,9 +155,7 @@ public static string MakeExceptionMessage(Exception exception, string linePrefix // Source: if (includedParts.HasFlag(MessageParts.Source) && exception.Source != null) - { sb.Append($"{tabPrefix}\"Source\": \"{exception.Source}\"{endline}"); - } // TargetSite: if (includedParts.HasFlag(MessageParts.TargetSite)) @@ -213,9 +211,7 @@ public static string MakeExceptionMessage(Exception exception, string linePrefix // InnerException: if (includedParts.HasFlag(MessageParts.InnerException) && exception.InnerException != null) - { sb.Append($"{tabPrefix}\"InnerException\": {MakeExceptionMessage(exception.InnerException, tabPrefix, endline, tabLength, includedParts)}{endline}"); - } // closing bracket sb.Append(linePrefix + '}'); diff --git a/VolumeControl.Log/Helpers/ThreadedActionQueue.cs b/VolumeControl.Log/Helpers/ThreadedActionQueue.cs new file mode 100644 index 000000000..7b1b396bb --- /dev/null +++ b/VolumeControl.Log/Helpers/ThreadedActionQueue.cs @@ -0,0 +1,115 @@ +namespace VolumeControl.Log.Helpers +{ + /// + /// Manages a background thread to execute queued actions. + /// + public abstract class ThreadedActionQueue : IDisposable + { + #region Constructor + /// + /// Instantiates a new instance. + /// + protected ThreadedActionQueue() + { + _thread = new Thread(new ThreadStart(ProcessQueue)) { IsBackground = true }; + // this is performed from a bg thread, to ensure the queue is serviced from a single thread + _thread.Start(); + } + #endregion Constructor + + #region Fields + private readonly Queue _queue = new(); + private readonly ManualResetEvent _itemAddedSignal = new(false); + private readonly ManualResetEvent _terminateSignal = new(false); + private readonly ManualResetEvent _isWaitingSignal = new(false); + private readonly Thread _thread; + #endregion Fields + + #region Methods + + #region (Private) ProcessQueue + private void ProcessQueue() + { + var waitHandles = new WaitHandle[] { _itemAddedSignal, _terminateSignal }; + Queue queueCopy; + while (true) + { + _isWaitingSignal.Set(); + int i = WaitHandle.WaitAny(waitHandles); + + if (i == 1) return; //< terminate was signaled + + _isWaitingSignal.Reset(); + _itemAddedSignal.Reset(); + + lock (_queue) + { + queueCopy = new(_queue); + _queue.Clear(); + } + + foreach (var action in queueCopy) + { + action.Invoke(); + } + } + } + #endregion (Private) ProcessQueue + + #region (Protected) Enqueue + /// + /// Adds the specified to the queue. + /// + /// A delegate to add to the action queue. + protected void Enqueue(Action action) + { + lock (_queue) + { + _queue.Enqueue(action); + } + _itemAddedSignal.Set(); + } + #endregion (Protected) Enqueue + + #region Flush + /// + /// Blocks the caller until the background thread has finished processing the queue. + /// + public void Flush() + => _isWaitingSignal.WaitOne(); + /// + /// Blocks the caller until the background thread has finished processing the queue, or until the specified timeout has elapsed. + /// + /// How many milliseconds to wait before returning early, or (-1) to wait indefinitely. + /// when the queue was successfully flushed before timing out; otherwise . + public bool Flush(int timeoutMs) + => _isWaitingSignal.WaitOne(timeoutMs); + /// + /// Blocks the caller until the background thread has finished processing the queue, or until the specified timeout has elapsed. + /// + /// How many milliseconds to wait before returning early, or (-1) to wait indefinitely. + /// to exit the synchronization domain for the context before waiting & reacquire it afterwards; otherwise, .
For more information, see this StackOverflow answer. + /// when the queue was successfully flushed before timing out; otherwise . + public bool Flush(int timeoutMs, bool exitContext) + => _isWaitingSignal.WaitOne(timeoutMs, exitContext); + #endregion Flush + + #endregion Methods + + #region IDisposable Implementation + /// + /// Calls . + /// + ~ThreadedActionQueue() => Dispose(); + /// + /// Terminates the background thread. Does not flush the queue. + /// + public void Dispose() + { + _terminateSignal.Set(); + _thread.Join(); + GC.SuppressFinalize(this); + } + #endregion IDisposable Implementation + } +} diff --git a/VolumeControl.Log/Interfaces/IEndpointWriter.cs b/VolumeControl.Log/Interfaces/IEndpointWriter.cs new file mode 100644 index 000000000..4e9c6f919 --- /dev/null +++ b/VolumeControl.Log/Interfaces/IEndpointWriter.cs @@ -0,0 +1,57 @@ +namespace VolumeControl.Log.Interfaces +{ + /// + /// Represents an endpoint that text can be written to. + /// + public interface IEndpointWriter + { + #region Properties + /// + /// Gets or sets whether this endpoint can be written to. + /// + bool IsWritingEnabled { get; set; } + #endregion Properties + + #region Events + /// + /// Occurs when the endpoint is about to be enabled or disabled for any reason. + /// + /// + /// The boolean argument is the incoming state. + /// + event EventHandler? EnabledChanging; + /// + /// Occurs when the endpoint is enabled or disabled for any reason. + /// + /// + /// The boolean argument is the new state. + /// + event EventHandler? EnabledChanged; + #endregion Events + + #region Methods + /// + /// Gets a object for writing to this endpoint. + /// + /// + /// The caller is responsible for disposing of the stream. + /// + /// A instance for this endpoint when it is enabled; otherwise . + TextWriter GetTextWriter(); + /// + /// Writes the specified to the endpoint. + /// + /// A string to write to the endpoint. + void Write(string @string); + /// + /// Writes the specified to the endpoint, followed by a line break. + /// + /// A string to write to the endpoint. + void WriteLine(string @string); + /// + /// Clears the endpoint's contents, leaving it blank. + /// + void Reset(); + #endregion Methods + } +} \ No newline at end of file diff --git a/VolumeControl.Log/ILogWriter.cs b/VolumeControl.Log/Interfaces/ILogWriter.cs similarity index 98% rename from VolumeControl.Log/ILogWriter.cs rename to VolumeControl.Log/Interfaces/ILogWriter.cs index f76049e78..e58906e92 100644 --- a/VolumeControl.Log/ILogWriter.cs +++ b/VolumeControl.Log/Interfaces/ILogWriter.cs @@ -1,6 +1,4 @@ -using VolumeControl.Log.Enum; - -namespace VolumeControl.Log +namespace VolumeControl.Log.Interfaces { /// /// Represents a log writer instance. diff --git a/VolumeControl.Log/LogMessage.cs b/VolumeControl.Log/LogMessage.cs index 06ab4feca..7b60be9ee 100644 --- a/VolumeControl.Log/LogMessage.cs +++ b/VolumeControl.Log/LogMessage.cs @@ -1,6 +1,4 @@ -using VolumeControl.Log.Enum; - -namespace VolumeControl.Log +namespace VolumeControl.Log { /// /// Represents a message to be written to the log. @@ -11,7 +9,7 @@ public sealed class LogMessage /// /// Creates a new instance with the specified . /// - /// The of this message. + /// The of this message. /// The lines in this message. public LogMessage(EventType eventType, params object?[] lines) { @@ -21,7 +19,7 @@ public LogMessage(EventType eventType, params object?[] lines) /// /// Creates a new empty instance with the specified . /// - /// The of this message. + /// The of this message. public LogMessage(EventType eventType) { EventType = eventType; diff --git a/VolumeControl.Log/SettingsInterface.cs b/VolumeControl.Log/SettingsInterface.cs deleted file mode 100644 index e1941be96..000000000 --- a/VolumeControl.Log/SettingsInterface.cs +++ /dev/null @@ -1,64 +0,0 @@ -using AppConfig; -using System.ComponentModel; -using VolumeControl.Log.Enum; - -namespace VolumeControl.Log -{ - /// - /// Provides a more convenient way to access the program configuration without having access to the VolumeControl.Core namespace.
- /// See the property. - ///
- internal class SettingsInterface : INotifyPropertyChanged - { - #region Constructor - private SettingsInterface() => Settings.PropertyChanged += this.HandleSettingsPropertyChanged; - #endregion Constructor - - #region Fields - private static readonly string[] _propertyNames = typeof(SettingsInterface).GetProperties().Where(p => p.CanRead && p.CanWrite).Select(p => p.Name).ToArray(); - #endregion Fields - - #region Events - public event PropertyChangedEventHandler? PropertyChanged; - private void NotifyPropertyChanged(object? sender, PropertyChangedEventArgs e) => PropertyChanged?.Invoke(sender, e); - private void HandleSettingsPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (_propertyNames.Any(n => n.Equals(e.PropertyName, StringComparison.Ordinal))) - { - this.NotifyPropertyChanged(sender, e); - } - } - #endregion Events - - #region Properties - /// - /// Default instance. - /// - public static SettingsInterface Default { get; } = new(); - private static Configuration Settings => Configuration.DefaultInstance; - - #region Settings - public bool EnableLogging - { - get => (bool)Settings[nameof(this.EnableLogging)]!; - set => Settings[nameof(this.EnableLogging)] = value; - } - public string LogPath - { - get => (string)Settings[nameof(this.LogPath)]!; - set => Settings[nameof(this.LogPath)] = value; - } - public EventType LogFilter - { - get => (EventType)Settings[nameof(this.LogFilter)]!; - set => Settings[nameof(this.LogFilter)] = value; - } - public bool LogClearOnInitialize - { - get => (bool)Settings[nameof(this.LogClearOnInitialize)]!; - set => Settings[nameof(this.LogClearOnInitialize)] = value; - } - #endregion Settings - #endregion Properties - } -} diff --git a/VolumeControl.Log/ThreadedLogger.cs b/VolumeControl.Log/ThreadedLogger.cs deleted file mode 100644 index 48ec3872d..000000000 --- a/VolumeControl.Log/ThreadedLogger.cs +++ /dev/null @@ -1,110 +0,0 @@ -namespace VolumeControl.Log -{ - /// - /// Manages a background thread to write queued log messages. The queue can be disabled at runtime to switch between asynchronous and synchronous operation. - /// - public abstract class ThreadedLogger : IDisposable - { - #region Constructor - /// - /// Instantiates a new instance. - /// - public ThreadedLogger() - { - _thread = new Thread(new ThreadStart(ProcessQueue)) { IsBackground = true }; - // this is performed from a bg thread, to ensure the queue is serviced from a single thread - _thread.Start(); - } - #endregion Constructor - - #region Fields - private readonly Queue _queue = new(); - private readonly ManualResetEvent _itemAddedSignal = new(false); - private readonly ManualResetEvent _terminateSignal = new(false); - private readonly ManualResetEvent _isWaitingSignal = new(false); - private readonly Thread _thread; - #endregion Fields - - #region Thread Method - private void ProcessQueue() - { - var waitHandles = new WaitHandle[] - { - _itemAddedSignal, - _terminateSignal - }; - Queue queueCopy; - while (true) - { - _isWaitingSignal.Set(); - int i = WaitHandle.WaitAny(waitHandles); - _isWaitingSignal.Reset(); - - if (i == 1) return; //< terminate was signaled - - _itemAddedSignal.Reset(); - - lock (_queue) - { - queueCopy = new Queue(_queue); - _queue.Clear(); - } - - foreach (var log in queueCopy) - { - log(); - } - } - } - #endregion Thread Method - - #region Methods - /// - /// Writes the specified to the log endpoint. - /// - /// The log message instance to write to the endpoint. - protected abstract void WriteLogMessage(LogMessage message); - /// - /// Adds the specified to the message queue when UseAsyncQueue is ; otherwise writes the message synchronously. - /// - /// A log message instance. - protected void LogMessage(LogMessage message) - { - message.RemoveNullLines(); //< remove all null lines - if (message.IsEmpty) return; - - lock (_queue) - { - _queue.Enqueue(() => WriteLogMessage(message)); - } - _itemAddedSignal.Set(); - } - /// - /// Flushes the message queue. - /// - public void Flush() - { - _isWaitingSignal.WaitOne(); - } - #endregion Methods - - #region IDisposable Implementation - bool disposed = false; - /// - /// Disposes of this instance, if it hasn't already been disposed. - /// - ~ThreadedLogger() - { - if (!disposed) Dispose(); - } - /// - public void Dispose() - { - disposed = true; - _terminateSignal.Set(); - _thread.Join(); - GC.SuppressFinalize(this); - } - #endregion IDisposable Implementation - } -} diff --git a/VolumeControl.Log/VolumeControl.Log.csproj b/VolumeControl.Log/VolumeControl.Log.csproj index 6d59f3a64..de69e99e0 100644 --- a/VolumeControl.Log/VolumeControl.Log.csproj +++ b/VolumeControl.Log/VolumeControl.Log.csproj @@ -14,17 +14,6 @@ snupkg - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - All - - - - diff --git a/VolumeControl.WPF/Behaviors/DisableMouseWheelBehavior.cs b/VolumeControl.WPF/Behaviors/DisableMouseWheelBehavior.cs new file mode 100644 index 000000000..ac69b0a4e --- /dev/null +++ b/VolumeControl.WPF/Behaviors/DisableMouseWheelBehavior.cs @@ -0,0 +1,29 @@ +using Microsoft.Xaml.Behaviors; +using System.Windows.Controls; + +namespace VolumeControl.WPF.Behaviors +{ + /// + /// Behavior that prevent the mouse wheel from triggering events. + /// + public class DisableMouseWheelBehavior : Behavior + { + #region Behavior Method Overrides + protected override void OnAttached() + { + base.OnAttached(); + AssociatedObject.PreviewMouseWheel += this.AssociatedObject_PreviewMouseWheel; + } + protected override void OnDetaching() + { + base.OnDetaching(); + AssociatedObject.PreviewMouseWheel -= this.AssociatedObject_PreviewMouseWheel; + } + #endregion Behavior Method Overrides + + #region EventHandlers + private void AssociatedObject_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e) + => e.Handled = true; + #endregion EventHandlers + } +} diff --git a/VolumeControl/App.xaml b/VolumeControl/App.xaml index 347a53068..a1c014699 100644 --- a/VolumeControl/App.xaml +++ b/VolumeControl/App.xaml @@ -755,8 +755,8 @@ @@ -900,17 +900,18 @@ + diff --git a/VolumeControl/App.xaml.cs b/VolumeControl/App.xaml.cs index f855771b3..f318ade8e 100644 --- a/VolumeControl/App.xaml.cs +++ b/VolumeControl/App.xaml.cs @@ -7,6 +7,7 @@ using System.Windows.Controls; using System.Windows.Input; using VolumeControl.Controls; +using VolumeControl.Core; using VolumeControl.Helpers; using VolumeControl.Log; using VolumeControl.ViewModels; @@ -39,6 +40,16 @@ public App() TrayIcon.ShowClicked += this.HandleTrayIconClick; TrayIcon.HideClicked += (s, e) => this.HideMainWindow(); TrayIcon.BringToFrontClicked += (s, e) => this.ActivateMainWindow(); + + TrayIcon.OpenConfigClicked += (s, e) => + { + Process.Start(new ProcessStartInfo(Config.Default.Location) { UseShellExecute = true }); + }; + TrayIcon.OpenLogClicked += (s, e) => + { + Process.Start(new ProcessStartInfo(Config.Default.LogPath) { UseShellExecute = true }); + }; + TrayIcon.OpenLocationClicked += (s, e) => { OpenFolderAndSelectItem(Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), Path.ChangeExtension(AppDomain.CurrentDomain.FriendlyName, ".exe")))); diff --git a/VolumeControl/Controls/VolumeControlNotifyIcon.cs b/VolumeControl/Controls/VolumeControlNotifyIcon.cs index f7eb3d392..92b9fed4f 100644 --- a/VolumeControl/Controls/VolumeControlNotifyIcon.cs +++ b/VolumeControl/Controls/VolumeControlNotifyIcon.cs @@ -16,6 +16,11 @@ public VolumeControlNotifyIcon(Query queryMainWindowVisible) { // TOOLSTRIP: new System.Windows.Forms.ToolStripButton(GetShowText(), Properties.Resources.foreground, this.HandleShowHideClick), new System.Windows.Forms.ToolStripButton(GetBringToFrontText(), Properties.Resources.bringtofront, this.HandleBringToFrontClick), + new System.Windows.Forms.ToolStripSeparator(), + + new System.Windows.Forms.ToolStripButton("Open Config", Properties.Resources.codefile, this.HandleOpenConfigClicked), + new System.Windows.Forms.ToolStripButton("Open Log", Properties.Resources.textfile, this.HandleOpenLogClicked), + new System.Windows.Forms.ToolStripSeparator(), new System.Windows.Forms.ToolStripButton(GetOpenAppDataText(), Properties.Resources.file_inverted, this.HandleOpenAppDataClick), new System.Windows.Forms.ToolStripButton(GetOpenLocationText(), Properties.Resources.file, this.HandleOpenLocationClick), @@ -44,6 +49,7 @@ public VolumeControlNotifyIcon(Query queryMainWindowVisible) #endregion TranslationGetters #region Events + private void Handle_CurrentLanguageChanged(object? sender, CurrentLanguageChangedEventArgs e) { this.Items[_idx_ShowHide].Text = GetShowHideText(_mainWindowVisibilityChecker()); @@ -82,10 +88,16 @@ private void HandleShowHideClick(object? sender, EventArgs e) private void HandleOpenLocationClick(object? sender, EventArgs e) => OpenLocationClicked?.Invoke(sender, e); private void HandleOpenAppDataClick(object? sender, EventArgs e) => OpenAppDataClicked?.Invoke(sender, e); private void HandleCloseClicked(object? sender, EventArgs e) => CloseClicked?.Invoke(sender, e); + private void HandleOpenConfigClicked(object? sender, EventArgs e) => OpenConfigClicked?.Invoke(sender, e); + private void HandleOpenLogClicked(object? sender, EventArgs e) => OpenLogClicked?.Invoke(sender, e); public event EventHandler? ShowClicked; public event EventHandler? HideClicked; public event EventHandler? BringToFrontClicked; + + public event EventHandler? OpenConfigClicked; + public event EventHandler? OpenLogClicked; + public event EventHandler? OpenLocationClicked; public event EventHandler? OpenAppDataClicked; public event EventHandler? CloseClicked; diff --git a/VolumeControl/Helpers/AddonLoader.cs b/VolumeControl/Helpers/AddonLoader.cs new file mode 100644 index 000000000..dad10c70a --- /dev/null +++ b/VolumeControl/Helpers/AddonLoader.cs @@ -0,0 +1,255 @@ +using CodingSeb.Localization.Loaders; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using VolumeControl.Core.Input; +using VolumeControl.Log; +using VolumeControl.TypeExtensions; +using VolumeControl.ViewModels; + +namespace VolumeControl.Helpers +{ + public class AddonLoader + { + #region Constructor + public AddonLoader() + { + ManifestResourceLoader = new((JsonFileLoader?)LocalizationLoader.FileLanguageLoaders.FirstOrDefault(loader => + loader.GetType().IsAssignableTo(typeof(JsonFileLoader))) ?? new JsonFileLoader()); + } + #endregion Constructor + + #region (class) ManifestResourceLocalizationLoader + class ManifestResourceLocalizationLoader + { + #region Constructor + public ManifestResourceLocalizationLoader(JsonFileLoader jsonFileLoader) + { + JsonFileLoader = jsonFileLoader; + } + #endregion Constructor + + #region Fields + private readonly JsonFileLoader JsonFileLoader; + #endregion Fields + + #region Methods + + #region LoadFromStream + /// + /// Loads all translations defined in Json format from the specified . + /// + /// + /// The caller is responsible for disposing of the . + /// + /// The stream to load translations from. + /// The loader to use for loading translations from the string. + /// Optional source file name. + public void LoadFromStream(Stream stream, LocalizationLoader loader, string resourceName) + { + using var reader = new StreamReader(stream, System.Text.Encoding.UTF8, true, leaveOpen: true); + string content = reader.ReadToEnd(); + LoadFromString(content, loader, resourceName); + } + #endregion LoadFromStream + + #region LoadFromString + /// + /// Load all translations defined in Json format from the specified . + /// + /// String to load serialized Json format translations from. + /// The loader to use for loading translations from the string. + /// Optional source file name. + public void LoadFromString(string jsonString, LocalizationLoader loader, string sourceFileName = "") + { + JObject root = (JObject)JsonConvert.DeserializeObject(jsonString)!; + + root.Properties().ToList() + .ForEach(property => ParseSubElement(property, new Stack(), loader, sourceFileName)); + } + #endregion LoadFromString + + #region ParseSubElement + private void ParseSubElement(JProperty property, Stack textId, LocalizationLoader loader, string source) + { + switch (property.Value.Type) + { + case JTokenType.Object: + textId.Push(property.Name); + ((JObject)property.Value).Properties().ToList() + .ForEach(subProperty => ParseSubElement(subProperty, textId, loader, source)); + textId.Pop(); + break; + case JTokenType.String: + + if (JsonFileLoader.LangIdDecoding == JsonFileLoaderLangIdDecoding.InFileNameBeforeExtension) + { + textId.Push(property.Name); + loader.AddTranslation( + JsonFileLoader.LabelPathRootPrefix + string.Join(JsonFileLoader.LabelPathSeparator, textId.Reverse()) + JsonFileLoader.LabelPathSuffix, + Path.GetExtension(Regex.Replace(source, @"\.loc\.json", "")).Replace(".", ""), + property.Value.ToString(), + source); + textId.Pop(); + } + else if (JsonFileLoader.LangIdDecoding == JsonFileLoaderLangIdDecoding.DirectoryName) + { + textId.Push(property.Name); + loader.AddTranslation(JsonFileLoader.LabelPathRootPrefix + string.Join(JsonFileLoader.LabelPathSeparator, textId.Reverse()) + JsonFileLoader.LabelPathSuffix, + Path.GetDirectoryName(source), + property.Value.ToString(), + source); + textId.Pop(); + } + else + { + loader.AddTranslation(JsonFileLoader.LabelPathRootPrefix + string.Join(JsonFileLoader.LabelPathSeparator, textId.Reverse()) + JsonFileLoader.LabelPathSuffix, property.Name, property.Value.ToString(), source); + } + break; + default: + throw new FormatException($"Invalid format in Json language file for property [{property.Name}]"); + } + } + #endregion ParseSubElement + + #endregion Methods + } + #endregion (class) ManifestResourceLocalizationLoader + + #region Fields + private static readonly Regex TranslationConfigAddonRegex = new(@"([a-z]{2}(?:-\w+){0,1}\.loc\.(?:json|yaml|yml))$", RegexOptions.Compiled); + private readonly ManifestResourceLocalizationLoader ManifestResourceLoader; + #endregion Fields + + #region Properties + private static LocalizationLoader LocalizationLoader => LocalizationLoader.Instance; + #endregion Properties + + #region Methods + + #region LoadTranslations + private void LoadTranslations(Assembly assembly) + { + foreach (var resourceName in assembly.GetManifestResourceNames()) + { + // filter out invalid names + var match = TranslationConfigAddonRegex.Match(resourceName); + if (!match.Success) + { + FLog.Debug($"[AddonLoader] Embedded resource \"{resourceName}\" does not have a valid name for a translation config, so it was not loaded."); + continue; + } + + // load translations + ManifestResourceLoader.LoadFromStream(assembly.GetManifestResourceStream(resourceName)!, LocalizationLoader, resourceName); + } + } + #endregion LoadTranslations + + #region LoadAddons + public void LoadAddons(VolumeControlVM inst) + { + // create a template provider manager + var templateProviderManager = new TemplateProviderManager(); + + var hotkeyActionsAssembly = Assembly.Load($"{nameof(VolumeControl)}.{nameof(HotkeyActions)}"); + + // load default addin translations + LoadTranslations(hotkeyActionsAssembly); + + // load default template providers + HotkeyActionAddonLoader.LoadProviders(ref templateProviderManager, + GetDataTemplateProviderTypes(Assembly.Load($"{nameof(VolumeControl)}.{nameof(SDK)}"))); + + // load default actions + inst.HotkeyAPI.HotkeyManager.HotkeyActionManager.AddActionDefinitions(HotkeyActionAddonLoader.LoadActions(templateProviderManager, + GetActionGroupTypes(hotkeyActionsAssembly))); + + // load custom addons + inst.AddonDirectories.ForEach(dir => + { + if (Directory.Exists(dir)) + { + FLog.Trace($"[AddonLoader] Searching for DLLs in directory \"{dir}\"."); + foreach (string dllPath in Directory.EnumerateFiles(dir, "*.dll", new EnumerationOptions() { MatchCasing = MatchCasing.CaseInsensitive, RecurseSubdirectories = false, })) + { + // print version info + var fileName = Path.GetFileName(dllPath); + FLog.Debug($"[AddonLoader] Found addon DLL \"{fileName}\""); + + var versionInfo = System.Diagnostics.FileVersionInfo.GetVersionInfo(dllPath); + if (versionInfo == null) + { + FLog.Debug($"[AddonLoader] Addon DLL \"{fileName}\" does not have any version information."); + } + else + { + var authorName = versionInfo.CompanyName; + + if (versionInfo.IsDebug) + { + FLog.Warning( + $"[AddonLoader] Addon DLL was built in DEBUG configuration \"{dllPath}\"!", + $" Contact {versionInfo.CompanyName ?? "the addon author"}"); + } + if (FLog.FilterEventType(EventType.DEBUG)) + { + + FLog.Debug($"[AddonLoader] Found addon DLL \"{versionInfo.FileName}\":", versionInfo.ToString()); + } + } + + // try loading the assembly + try + { + var asm = Assembly.LoadFrom(dllPath); + var assemblyName = asm.FullName ?? dllPath; + var exportedTypes = asm.GetExportedTypes(); + asm = null; + + FLog.Debug($"[AddonLoader] \"{assemblyName}\" exports {exportedTypes.Length} types."); + + // load providers from addon assembly + HotkeyActionAddonLoader.LoadProviders(ref templateProviderManager, exportedTypes); + + // load actions from addon assembly + inst.HotkeyAPI.HotkeyManager.HotkeyActionManager.AddActionDefinitions(HotkeyActionAddonLoader.LoadActions(templateProviderManager, exportedTypes)); + } + catch (Exception ex) + { + FLog.Critical($"[AddonLoader] Failed to load addon DLL \"{dllPath}\" due to an exception!", ex); + } + } + } + else + { + FLog.Trace($"[AddonLoader] Addon directory \"{dir}\" doesn't exist."); + } + }); + } + #endregion LoadAddons + + #region GetActionGroupTypes + /// + /// Gets the hotkey action types from the specified . + /// + private static Type[] GetActionGroupTypes(Assembly assembly) + => assembly.GetExportedTypes().Where(type => type.GetCustomAttribute() != null).ToArray(); + #endregion GetActionGroupTypes + + #region GetDataTemplateProviderTypes + /// + /// Gets the data template provider types from the specified . + /// + private static Type[] GetDataTemplateProviderTypes(Assembly assembly) + => assembly.GetExportedTypes().Where(type => type.GetCustomAttribute() != null).ToArray(); + #endregion GetDataTemplateProviderTypes + + #endregion Methods + } +} diff --git a/VolumeControl/Helpers/LocalizationHelper.cs b/VolumeControl/Helpers/LocalizationHelper.cs index 54eb54770..87bf0f4e6 100644 --- a/VolumeControl/Helpers/LocalizationHelper.cs +++ b/VolumeControl/Helpers/LocalizationHelper.cs @@ -8,6 +8,7 @@ using System.Text.RegularExpressions; using VolumeControl.Core; using VolumeControl.Log; +using VolumeControl.Log.Interfaces; using VolumeControl.TypeExtensions; namespace VolumeControl.Helpers @@ -22,9 +23,6 @@ public LocalizationHelper(bool overwriteExistingConfigs, ILogWriter? log = null) _initialized = true; - _ = FileLoaders.AddIfUnique(new JsonFileLoader()); - _ = FileLoaders.AddIfUnique(new YamlFileLoader()); - if (Settings.CreateDefaultTranslationFiles) //< never create default files when this (and this alone) is false! CreateDefaultFiles(overwriteExistingConfigs); @@ -51,10 +49,9 @@ public LocalizationHelper(bool overwriteExistingConfigs, ILogWriter? log = null) #region Properties private static Config Settings => Config.Default; private static LocalizationLoader Loader => LocalizationLoader.Instance; - private static List FileLoaders => Loader.FileLanguageLoaders; private static string DefaultPath { get; } = Path.Combine(PathFinder.ApplicationAppDataPath, "Localization"); private static Loc Loc => Loc.Instance; - private static readonly Regex LanguageConfigNameRegex = new(@"([a-z]{2}\.loc\.(?:json|yaml|yml))", RegexOptions.Compiled); + private static readonly Regex LanguageConfigNameRegex = new(@"([a-z]{2}\.loc\.(?:json|yaml|yml))$", RegexOptions.Compiled); #endregion Properties #region Methods @@ -82,7 +79,7 @@ public static void ReloadLanguageConfigs(ILogWriter? log) private static void LoadTranslationsFromDirectory(string path, ILogWriter? log) { - bool showTraceLogMessages = log?.FilterEventType(Log.Enum.EventType.TRACE) ?? false; + bool showTraceLogMessages = log?.FilterEventType(EventType.TRACE) ?? false; if (Directory.Exists(path)) { if (showTraceLogMessages) diff --git a/VolumeControl/Mixer.xaml b/VolumeControl/Mixer.xaml index 37b169aba..fea155035 100644 --- a/VolumeControl/Mixer.xaml +++ b/VolumeControl/Mixer.xaml @@ -15,6 +15,7 @@ xmlns:loc="clr-namespace:CodingSeb.Localization;assembly=CodingSeb.Localization" xmlns:local="clr-namespace:VolumeControl" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:rsc="clr-namespace:VolumeControl.Properties" xmlns:sys="clr-namespace:System;assembly=netstandard" xmlns:vm="clr-namespace:VolumeControl.ViewModels" xmlns:wpf="clr-namespace:VolumeControl.WPF;assembly=VolumeControl.WPF" @@ -942,19 +943,22 @@ ItemsSource="{StaticResource KeyOptions}" SelectedValue="{Binding Hotkey.Key, UpdateSourceTrigger=PropertyChanged}" SelectedValuePath="Key" - Style="{StaticResource ComboBoxStyle}" /> + Style="{StaticResource ComboBoxStyle}"> + + + +