diff --git a/Source/Installer/Installer.wixproj b/Source/Installer/Installer.wixproj index b622f0a6..328c48c3 100644 --- a/Source/Installer/Installer.wixproj +++ b/Source/Installer/Installer.wixproj @@ -39,6 +39,20 @@ INSTALLFOLDER + + + $(WixExtDir)\WixUIExtension.dll + WixUIExtension + + + + + + + + + + + + + + + + + diff --git a/Source/Installer/Resources/License.rtf b/Source/Installer/Resources/License.rtf new file mode 100644 index 00000000..5e8043ab Binary files /dev/null and b/Source/Installer/Resources/License.rtf differ diff --git a/Source/Installer/Resources/banner.png b/Source/Installer/Resources/banner.png new file mode 100644 index 00000000..832be823 Binary files /dev/null and b/Source/Installer/Resources/banner.png differ diff --git a/Source/Installer/Resources/dialog.png b/Source/Installer/Resources/dialog.png new file mode 100644 index 00000000..f3776290 Binary files /dev/null and b/Source/Installer/Resources/dialog.png differ diff --git a/Source/Monitorian.Core/AppControllerCore.cs b/Source/Monitorian.Core/AppControllerCore.cs index 87d9ce03..ed1b53aa 100644 --- a/Source/Monitorian.Core/AppControllerCore.cs +++ b/Source/Monitorian.Core/AppControllerCore.cs @@ -60,7 +60,7 @@ public AppControllerCore(AppKeeper keeper, SettingsCore settings) public virtual async Task InitiateAsync() { - Settings.Initiate(); + await Settings.InitiateAsync(); Settings.MonitorCustomizations.AbsoluteCapacity = MaxKnownMonitorsCount; Settings.PropertyChanged += OnSettingsChanged; @@ -145,13 +145,13 @@ protected virtual void ShowMenuWindow(Point pivot) window.Show(); } - protected virtual void OnSettingsInitiated() + protected virtual async void OnSettingsInitiated() { if (Settings.MakesOperationLog) - Recorder = new("Initiated"); + Recorder = await OperationRecorder.CreateAsync("Initiated"); } - protected virtual void OnSettingsChanged(object sender, PropertyChangedEventArgs e) + protected virtual async void OnSettingsChanged(object sender, PropertyChangedEventArgs e) { switch (e.PropertyName) { @@ -160,7 +160,7 @@ protected virtual void OnSettingsChanged(object sender, PropertyChangedEventArgs break; case nameof(Settings.MakesOperationLog): - Recorder = Settings.MakesOperationLog ? new("Enabled") : null; + Recorder = Settings.MakesOperationLog ? await OperationRecorder.CreateAsync("Enabled") : null; break; } } @@ -169,7 +169,7 @@ protected virtual void OnSettingsChanged(object sender, PropertyChangedEventArgs protected virtual async void OnMonitorsChangeInferred(object sender = null, PowerModes mode = default, int? count = null) { - Recorder?.Record($"{nameof(OnMonitorsChangeInferred)} ({sender}{(mode == default ? string.Empty : $"- {mode} {count}")})"); + await (Recorder?.RecordAsync($"{nameof(OnMonitorsChangeInferred)} ({sender}{(mode == default ? string.Empty : $"- {mode} {count}")})") ?? Task.CompletedTask); if (count == 0) return; @@ -177,11 +177,18 @@ protected virtual async void OnMonitorsChangeInferred(object sender = null, Powe await ScanAsync(TimeSpan.FromSeconds(3)); } - protected internal virtual void OnMonitorsChangeFound() + protected internal virtual async void OnMonitorAccessFailed(AccessResult result) + { + await (Recorder?.RecordAsync($"{nameof(OnMonitorAccessFailed)}" + Environment.NewLine + + $"Status: {result.Status}" + Environment.NewLine + + $"Message: {result.Message}") ?? Task.CompletedTask); + } + + protected internal virtual async void OnMonitorsChangeFound() { if (Monitors.Any()) { - Recorder?.Record($"{nameof(OnMonitorsChangeFound)}"); + await (Recorder?.RecordAsync($"{nameof(OnMonitorsChangeFound)}") ?? Task.CompletedTask); _displayWatcher.RaiseDisplaySettingsChanged(); } @@ -270,7 +277,7 @@ await Task.Run(async () => var maxMonitorsCount = await GetMaxMonitorsCountAsync(); var updateResults = await Task.WhenAll(Monitors - .Where(x => x.IsLikelyControllable) + .Where(x => x.IsReachable) .Select((x, index) => { if (index < maxMonitorsCount) diff --git a/Source/Monitorian.Core/Helper/ArraySearch.cs b/Source/Monitorian.Core/Helper/ArraySearch.cs index 8904a360..7abaa87a 100644 --- a/Source/Monitorian.Core/Helper/ArraySearch.cs +++ b/Source/Monitorian.Core/Helper/ArraySearch.cs @@ -26,7 +26,7 @@ public static int GetNearestIndex(byte[] array, byte target) => public static int GetNearestIndex(T[] array, T target, Func measure) where T : IComparable { - if ((array is null) || (array.Length == 0)) + if (array is null or { Length: 0 }) throw new ArgumentNullException(nameof(array)); // The source array must be sorted beforehand. diff --git a/Source/Monitorian.Core/Models/AppDataService.cs b/Source/Monitorian.Core/Models/AppDataService.cs index f38c2eb6..d49eff09 100644 --- a/Source/Monitorian.Core/Models/AppDataService.cs +++ b/Source/Monitorian.Core/Models/AppDataService.cs @@ -30,7 +30,56 @@ public static void AssureFolder() Directory.CreateDirectory(FolderPath); } - public static void Load(T instance, string fileName) where T : class + #region Access + + public static IEnumerable EnumerateFileNames(string searchPattern) + { + if (!Directory.Exists(FolderPath)) + return Enumerable.Empty(); + + return Directory.EnumerateFiles(FolderPath, searchPattern) + .Select(x => Path.GetFileName(x)); + } + + public static async Task ReadAsync(string fileName) + { + var filePath = Path.Combine(FolderPath, fileName); + + if (!File.Exists(filePath)) + return null; + + using var sr = new StreamReader(filePath, Encoding.UTF8); + return await sr.ReadToEndAsync(); + } + + public static async Task WriteAsync(string fileName, bool append, string content) + { + AssureFolder(); + + var filePath = Path.Combine(FolderPath, fileName); + + using var sw = new StreamWriter(filePath, append, Encoding.UTF8); // BOM will be emitted. + await sw.WriteAsync(content); + } + + public static void Delete(string fileName) + { + var filePath = Path.Combine(FolderPath, fileName); + File.Delete(filePath); + } + + public static void Rename(string oldFileName, string newFileName) + { + var oldFilePath = Path.Combine(FolderPath, oldFileName); + var newFilePath = Path.Combine(FolderPath, newFileName); + File.Move(oldFilePath, newFilePath); + } + + #endregion + + #region Load/Save + + public static void Load(T instance, string fileName, IEnumerable knownTypes = null) where T : class { var filePath = Path.Combine(FolderPath, fileName); var fileInfo = new FileInfo(filePath); @@ -43,7 +92,7 @@ public static void Load(T instance, string fileName) where T : class using var xr = XmlReader.Create(sr); var type = instance.GetType(); // GetType method works in derived class. - var serializer = new DataContractSerializer(type); + var serializer = new DataContractSerializer(type, knownTypes); var loaded = (T)serializer.ReadObject(xr); type.GetProperties(BindingFlags.Public | BindingFlags.Instance) @@ -61,7 +110,7 @@ public static void Load(T instance, string fileName) where T : class } } - public static void Save(T instance, string fileName) where T : class + public static void Save(T instance, string fileName, IEnumerable knownTypes = null) where T : class { AssureFolder(); @@ -71,9 +120,11 @@ public static void Save(T instance, string fileName) where T : class using var xw = XmlWriter.Create(sw, new XmlWriterSettings { Indent = true }); var type = instance.GetType(); // GetType method works in derived class. - var serializer = new DataContractSerializer(type); + var serializer = new DataContractSerializer(type, knownTypes); serializer.WriteObject(xw, instance); xw.Flush(); } + + #endregion } } \ No newline at end of file diff --git a/Source/Monitorian.Core/Models/LogService.cs b/Source/Monitorian.Core/Models/LogService.cs index e46c723c..ed046d7b 100644 --- a/Source/Monitorian.Core/Models/LogService.cs +++ b/Source/Monitorian.Core/Models/LogService.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; @@ -14,13 +16,13 @@ namespace Monitorian.Core.Models { public class LogService { - private const string ProbeFileName = "probe.log"; - private const string OperationFileName = "operation.log"; - private const string ExceptionFileName = "exception.log"; - private const string HeaderStart = "[Date:"; private static string ComposeHeader() => $"{HeaderStart} {DateTime.Now} Ver: {ProductInfo.Version}]"; + #region Probe + + private const string ProbeFileName = "probe.log"; + /// /// Records probe log to Desktop. /// @@ -44,32 +46,122 @@ public static void RecordProbe(string content) RecordToDesktop(ProbeFileName, content); } + #endregion + + #region Operation + + private const string OperationFileName = "operation.log"; + private const string DateFormat = "yyyyMMdd"; + private static string GetOperationDateFileName(DateTimeOffset date) => $"operation{date.ToString(DateFormat)}.log"; + + /// + /// The number of days before expiration of operation log file + /// + public static byte ExpiryDays { get; set; } = 6; + + public static Task PrepareOperationAsync() + { + try + { + return GetOperationFileNamesAsync(); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + private static async Task GetOperationFileNamesAsync() + { + var expiryDate = DateTimeOffset.Now.Date.AddDays(-ExpiryDays); + var fileNamePattern = new Regex(@"^operation(?[0-9]{8}).log$"); + var fileNames = new List(); + + foreach (var fileName in AppDataService.EnumerateFileNames("*.log").OrderBy(x => x)) + { + var match = fileNamePattern.Match(fileName); + if (match.Success) + { + if (DateTimeOffset.TryParseExact(match.Groups["date"].Value, + DateFormat, + null, + DateTimeStyles.None, + out DateTimeOffset fileDate) + && (expiryDate <= fileDate)) + { + fileNames.Add(fileName); + continue; + } + + AppDataService.Delete(fileName); + } + } + + #region Conversion + + var content = await AppDataService.ReadAsync(OperationFileName); + if (content is not null) + { + if (fileNames.Any()) + { + content += await AppDataService.ReadAsync(fileNames.First()); + await AppDataService.WriteAsync(fileNames.First(), append: false, content); + AppDataService.Delete(OperationFileName); + } + else + { + var fileName = GetOperationDateFileName(DateTimeOffset.Now); + AppDataService.Rename(OperationFileName, fileName); + fileNames.Add(fileName); + } + } + + #endregion + + return fileNames.ToArray(); + } + /// /// Records operation log to AppData. /// /// Content - /// The number of entries that the log file can contain - /// - /// The log file will be appended with new content as long as one day has not yet passed - /// since last write. Otherwise, the log file will be overwritten. - /// - public static void RecordOperation(string content, int capacity = 128) + public static async Task RecordOperationAsync(string content) { - if (capacity <= 0) - throw new ArgumentOutOfRangeException(nameof(capacity), capacity, "The capacity must be positive."); - content = ComposeHeader() + Environment.NewLine + content + Environment.NewLine + Environment.NewLine; - RecordToAppData(OperationFileName, content, capacity); + var fileName = GetOperationDateFileName(DateTimeOffset.Now); + + try + { + await AppDataService.WriteAsync(fileName, append: true, content); + } + catch (Exception ex) + { + Trace.WriteLine("Failed to record log to AppData." + Environment.NewLine + + ex); + } } /// - /// Copies operation log to Desktop. + /// Copies operation log from AppData to Desktop. /// - public static void CopyOperation() + public static async Task CopyOperationAsync() { - if (!TryReadFromAppData(OperationFileName, out string content)) + var buffer = new StringBuilder(); + + try + { + foreach (var fileName in await GetOperationFileNamesAsync()) + buffer.Append(await AppDataService.ReadAsync(fileName)); + } + catch (Exception ex) + { + Trace.WriteLine("Failed to read log from AppData." + Environment.NewLine + + ex); + } + + if (buffer.Length == 0) return; if (MessageBox.Show( @@ -80,9 +172,15 @@ public static void CopyOperation() MessageBoxResult.OK) != MessageBoxResult.OK) return; - RecordToDesktop(OperationFileName, content); + RecordToDesktop(OperationFileName, buffer.ToString()); } + #endregion + + #region Exception + + private const string ExceptionFileName = "exception.log"; + /// /// Records exception log to AppData and Desktop. /// @@ -115,6 +213,8 @@ public static void RecordException(Exception exception, int capacity = 8) RecordToDesktop(ExceptionFileName, content, capacity); } + #endregion + #region Helper private static void RecordToTemp(string fileName, string content, int capacity = 1) @@ -123,7 +223,7 @@ private static void RecordToTemp(string fileName, string content, int capacity = { var tempFilePath = Path.Combine(Path.GetTempPath(), fileName); - UpdateText(tempFilePath, content, capacity); + UpdateContent(tempFilePath, content, capacity); } catch (Exception ex) { @@ -138,11 +238,9 @@ private static void RecordToAppData(string fileName, string content, int capacit { AppDataService.AssureFolder(); - var appDataFilePath = Path.Combine( - AppDataService.FolderPath, - fileName); + var appDataFilePath = Path.Combine(AppDataService.FolderPath, fileName); - UpdateText(appDataFilePath, content, capacity); + UpdateContent(appDataFilePath, content, capacity); } catch (Exception ex) { @@ -151,32 +249,6 @@ private static void RecordToAppData(string fileName, string content, int capacit } } - private static bool TryReadFromAppData(string fileName, out string content) - { - var appDataFilePath = Path.Combine( - AppDataService.FolderPath, - fileName); - - if (File.Exists(appDataFilePath)) - { - try - { - using (var sr = new StreamReader(appDataFilePath, Encoding.UTF8)) - { - content = sr.ReadToEnd(); - return true; - } - } - catch (Exception ex) - { - Trace.WriteLine("Failed to read log from AppData." + Environment.NewLine - + ex); - } - } - content = null; - return false; - } - private static void RecordToDesktop(string fileName, string content, int capacity = 1) { try @@ -185,7 +257,7 @@ private static void RecordToDesktop(string fileName, string content, int capacit Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), fileName); - UpdateText(desktopFilePath, content, capacity); + UpdateContent(desktopFilePath, content, capacity); } catch (Exception ex) { @@ -194,42 +266,40 @@ private static void RecordToDesktop(string fileName, string content, int capacit } } - private static void SaveText(string filePath, string content) - { - using (var sw = new StreamWriter(filePath, false, Encoding.UTF8)) // BOM will be emitted. - sw.Write(content); - } - - private static void UpdateText(string filePath, string newContent, int capacity) + /// + /// Updates content of a log file. + /// + /// File path + /// New content + /// The number of entries that the log file can contain + /// + /// The log file will be appended with new content as long as one day has not yet passed + /// since last write. Otherwise, the log file will be overwritten. + /// + private static void UpdateContent(string filePath, string newContent, int capacity) { string oldContent = null; if ((1 < capacity) && File.Exists(filePath) && (File.GetLastWriteTime(filePath) > DateTime.Now.AddDays(-1))) { - using (var sr = new StreamReader(filePath, Encoding.UTF8)) - oldContent = sr.ReadToEnd(); + using var sr = new StreamReader(filePath, Encoding.UTF8); + oldContent = sr.ReadToEnd(); - oldContent = TruncateSections(oldContent, HeaderStart, capacity - 1); + if (!string.IsNullOrEmpty(oldContent)) + oldContent = TruncateSections(oldContent, HeaderStart, capacity - 1); } - SaveText(filePath, oldContent + newContent); - } - - private static string TruncateSections(string source, string sectionHeader, int sectionCount) - { - if (string.IsNullOrEmpty(sectionHeader)) - throw new ArgumentNullException(nameof(sectionHeader)); - if (sectionCount <= 0) - throw new ArgumentOutOfRangeException(nameof(sectionCount), sectionCount, "The count must be greater than 0."); + using var sw = new StreamWriter(filePath, false, Encoding.UTF8); // BOM will be emitted. + sw.Write(oldContent + newContent); - if (string.IsNullOrEmpty(source)) - return string.Empty; - - var firstIndex = source.StartsWith(sectionHeader, StringComparison.Ordinal) ? new[] { 0 } : Enumerable.Empty(); - var secondIndices = source.IndicesOf('\n' /* either CR+Lf or Lf */ + sectionHeader, StringComparison.Ordinal).Select(x => x + 1); - var indices = firstIndex.Concat(secondIndices).ToArray(); + static string TruncateSections(string content, string sectionHeader, int sectionCount) + { + var firstIndex = content.StartsWith(sectionHeader, StringComparison.Ordinal) ? new[] { 0 } : Enumerable.Empty(); + var secondIndices = content.IndicesOf('\n' /* either CR+Lf or Lf */ + sectionHeader, StringComparison.Ordinal).Select(x => x + 1); + var indices = firstIndex.Concat(secondIndices).ToArray(); - return source.Substring(indices[Math.Max(0, indices.Length - sectionCount)]); + return content.Substring(indices[Math.Max(0, indices.Length - sectionCount)]); + } } #endregion diff --git a/Source/Monitorian.Core/Models/Monitor/DdcMonitorItem.cs b/Source/Monitorian.Core/Models/Monitor/DdcMonitorItem.cs index 21b4489a..97a84921 100644 --- a/Source/Monitorian.Core/Models/Monitor/DdcMonitorItem.cs +++ b/Source/Monitorian.Core/Models/Monitor/DdcMonitorItem.cs @@ -13,7 +13,7 @@ namespace Monitorian.Core.Models.Monitor internal class DdcMonitorItem : MonitorItem { private readonly SafePhysicalMonitorHandle _handle; - private readonly bool _useLowLevel; + private readonly bool _useHighLevel; public DdcMonitorItem( string deviceInstanceId, @@ -21,7 +21,7 @@ public DdcMonitorItem( byte displayIndex, byte monitorIndex, SafePhysicalMonitorHandle handle, - bool useLowLevel = false) : base( + bool useHighLevel = true) : base( deviceInstanceId: deviceInstanceId, description: description, displayIndex: displayIndex, @@ -29,7 +29,7 @@ public DdcMonitorItem( isReachable: true) { this._handle = handle ?? throw new ArgumentNullException(nameof(handle)); - this._useLowLevel = useLowLevel; + this._useHighLevel = useHighLevel; } private uint _minimum = 0; // Raw minimum brightness (not always 0) @@ -37,9 +37,9 @@ public DdcMonitorItem( public override AccessResult UpdateBrightness(int brightness = -1) { - var (result, minimum, current, maximum) = MonitorConfiguration.GetBrightness(_handle, _useLowLevel); + var (result, minimum, current, maximum) = MonitorConfiguration.GetBrightness(_handle, _useHighLevel); - if ((result == AccessResult.Succeeded) && (minimum < maximum) && (minimum <= current) && (current <= maximum)) + if ((result.Status == AccessStatus.Succeeded) && (minimum < maximum) && (minimum <= current) && (current <= maximum)) { this.Brightness = (int)Math.Round((double)(current - minimum) / (maximum - minimum) * 100D, MidpointRounding.AwayFromZero); this._minimum = minimum; @@ -47,7 +47,7 @@ public override AccessResult UpdateBrightness(int brightness = -1) } else { - this.Brightness = -1; + this.Brightness = -1; // Default } return result; } @@ -59,9 +59,9 @@ public override AccessResult SetBrightness(int brightness) var buffer = (uint)Math.Round(brightness / 100D * (_maximum - _minimum) + _minimum, MidpointRounding.AwayFromZero); - var result = MonitorConfiguration.SetBrightness(_handle, buffer, _useLowLevel); + var result = MonitorConfiguration.SetBrightness(_handle, buffer, _useHighLevel); - if (result == AccessResult.Succeeded) + if (result.Status == AccessStatus.Succeeded) { this.Brightness = brightness; } diff --git a/Source/Monitorian.Core/Models/Monitor/DisplayConfig.cs b/Source/Monitorian.Core/Models/Monitor/DisplayConfig.cs index 88a5f1d6..42070df8 100644 --- a/Source/Monitorian.Core/Models/Monitor/DisplayConfig.cs +++ b/Source/Monitorian.Core/Models/Monitor/DisplayConfig.cs @@ -262,36 +262,41 @@ private enum DISPLAYCONFIG_DEVICE_INFO_TYPE : uint #region Type [DataContract] - public class ConfigItem + public class DisplayItem : IDisplayItem { [DataMember(Order = 0)] public string DeviceInstanceId { get; } [DataMember(Order = 1)] - public string FriendlyName { get; } + public string DisplayName { get; } [DataMember(Order = 2)] public bool IsInternal { get; } [DataMember(Order = 3)] + public string ConnectionDescription { get; } + + [DataMember(Order = 4)] public bool IsAvailable { get; } - public ConfigItem( + public DisplayItem( string deviceInstanceId, - string friendlyName, + string displayName, bool isInternal, + string connectionDescription, bool isAvailable) { this.DeviceInstanceId = deviceInstanceId; - this.FriendlyName = friendlyName; + this.DisplayName = displayName; this.IsInternal = isInternal; + this.ConnectionDescription = connectionDescription; this.IsAvailable = isAvailable; } } #endregion - public static IEnumerable EnumerateDisplayConfigs() + public static IEnumerable EnumerateDisplayConfigs() { if (GetDisplayConfigBufferSizes( QDC_ONLY_ACTIVE_PATHS, @@ -330,12 +335,40 @@ public static IEnumerable EnumerateDisplayConfigs() var deviceInstanceId = DeviceConversion.ConvertToDeviceInstanceId(deviceName.monitorDevicePath); - yield return new ConfigItem( + yield return new DisplayItem( deviceInstanceId: deviceInstanceId, - friendlyName: deviceName.monitorFriendlyDeviceName, + displayName: deviceName.monitorFriendlyDeviceName, isInternal: (deviceName.outputTechnology == DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL), + connectionDescription: GetConnectionDescription(deviceName.outputTechnology), isAvailable: displayPath.targetInfo.targetAvailable); } } + + private static string GetConnectionDescription(DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY outputTechnology) + { + return outputTechnology switch + { + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_OTHER => "Other", + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HD15 => "VGA", + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SVIDEO or + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_COMPOSITE_VIDEO or + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_COMPONENT_VIDEO or + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_D_JPN => "AnalogTV", + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DVI => "DVI", + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI => "HDMI", + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_LVDS => "LVDS", + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SDI => "SDI", + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL or + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EMBEDDED => "DisplayPort", + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EXTERNAL or + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EMBEDDED => "UDI", + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SDTVDONGLE => "SDTV", + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_MIRACAST => "Miracast", + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INDIRECT_WIRED => "Wired", + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INDIRECT_VIRTUAL => "Virtual", + DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY.DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL => "Internal", + _ => null + }; + } } } \ No newline at end of file diff --git a/Source/Monitorian.Core/Models/Monitor/DisplayMonitor.cs b/Source/Monitorian.Core/Models/Monitor/DisplayMonitor.cs new file mode 100644 index 00000000..13e224ed --- /dev/null +++ b/Source/Monitorian.Core/Models/Monitor/DisplayMonitor.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace Monitorian.Core.Models.Monitor +{ + internal interface IDisplayItem + { + public string DeviceInstanceId { get; } + public string DisplayName { get; } + public bool IsInternal { get; } + public string ConnectionDescription { get; } + } + + internal class DisplayMonitor + { + #region Type + + [DataContract] + public class DisplayItem : IDisplayItem + { + [DataMember(Order = 0)] + public string DeviceInstanceId { get; } + + [DataMember(Order = 1)] + public string DisplayName { get; } + + [DataMember(Order = 2)] + public bool IsInternal { get; } + + [DataMember(Order = 3)] + public string ConnectionDescription { get; } + + [DataMember(Order = 4)] + public float PhysicalSize { get; } + + public DisplayItem(Monitorian.Supplement.DisplayInformation.DisplayItem item) + { + this.DeviceInstanceId = item.DeviceInstanceId; + this.DisplayName = item.DisplayName; + this.IsInternal = item.IsInternal; + this.ConnectionDescription = item.ConnectionDescription; + this.PhysicalSize = item.PhysicalSize; + } + } + + #endregion + + public static Task GetDisplayMonitorsAsync() + { + return Monitorian.Supplement.DisplayInformation.GetDisplayMonitorsAsync() + .ContinueWith(task => task.Result.Select(x => new DisplayItem(x)).ToArray()); + } + } +} \ No newline at end of file diff --git a/Source/Monitorian.Core/Models/Monitor/Error.cs b/Source/Monitorian.Core/Models/Monitor/Error.cs index d2cbc19d..ecf734b5 100644 --- a/Source/Monitorian.Core/Models/Monitor/Error.cs +++ b/Source/Monitorian.Core/Models/Monitor/Error.cs @@ -25,10 +25,10 @@ private static extern uint FormatMessage( #endregion - public static (string message, int errorCode) GetMessageCode() + public static (int errorCode, string message) GetCodeMessage() { var errorCode = Marshal.GetLastWin32Error(); - return (GetMessage(errorCode), errorCode); + return (errorCode, GetMessage(errorCode)); } public static string GetMessage() => GetMessage(Marshal.GetLastWin32Error()); diff --git a/Source/Monitorian.Core/Models/Monitor/IMonitor.cs b/Source/Monitorian.Core/Models/Monitor/IMonitor.cs index fca9d625..4cd7f677 100644 --- a/Source/Monitorian.Core/Models/Monitor/IMonitor.cs +++ b/Source/Monitorian.Core/Models/Monitor/IMonitor.cs @@ -21,7 +21,7 @@ public interface IMonitor : IDisposable AccessResult SetBrightness(int brightness); } - public enum AccessResult + public enum AccessStatus { None = 0, Succeeded, @@ -29,4 +29,15 @@ public enum AccessResult DdcFailed, NoLongerExist } + + public class AccessResult + { + public AccessStatus Status { get; } + public string Message { get; } + + public AccessResult(AccessStatus status, string message) => (this.Status, this.Message) = (status, message); + + public static readonly AccessResult Succeeded = new(AccessStatus.Succeeded, null); + public static readonly AccessResult Failed = new(AccessStatus.Failed, null); + } } \ No newline at end of file diff --git a/Source/Monitorian.Core/Models/Monitor/MonitorConfiguration.cs b/Source/Monitorian.Core/Models/Monitor/MonitorConfiguration.cs index e753b4fe..9c38d8e2 100644 --- a/Source/Monitorian.Core/Models/Monitor/MonitorConfiguration.cs +++ b/Source/Monitorian.Core/Models/Monitor/MonitorConfiguration.cs @@ -348,7 +348,7 @@ private static IEnumerable EnumerateVcpCodes(string source) /// Gets raw brightnesses not represented in percentage. /// /// Physical monitor handle - /// Whether to use low level function + /// Whether to use high level function /// /// result: Result /// minimum: Raw minimum brightness (not always 0) @@ -361,7 +361,7 @@ private static IEnumerable EnumerateVcpCodes(string source) /// in percentage using those values. They are used to convert brightness in percentage /// back to raw brightness when settings brightness as well. /// - public static (AccessResult result, uint minimum, uint current, uint maximum) GetBrightness(SafePhysicalMonitorHandle physicalMonitorHandle, bool useLowLevel = false) + public static (AccessResult result, uint minimum, uint current, uint maximum) GetBrightness(SafePhysicalMonitorHandle physicalMonitorHandle, bool useHighLevel = true) { if (physicalMonitorHandle is null) throw new ArgumentNullException(nameof(physicalMonitorHandle)); @@ -372,7 +372,7 @@ public static (AccessResult result, uint minimum, uint current, uint maximum) Ge return (result: AccessResult.Failed, 0, 0, 0); } - if (!useLowLevel) + if (useHighLevel) { if (GetMonitorBrightness( physicalMonitorHandle, @@ -385,9 +385,9 @@ public static (AccessResult result, uint minimum, uint current, uint maximum) Ge current: currentBrightness, maximum: maximumBrightness); } - var (message, errorCode) = Error.GetMessageCode(); + var (errorCode, message) = Error.GetCodeMessage(); Debug.WriteLine($"Failed to get brightnesses (High level). {message}"); - return (result: GetResult(errorCode), 0, 0, 0); + return (result: new AccessResult(GetStatus(errorCode), $"High level, {message}"), 0, 0, 0); } else { @@ -403,9 +403,9 @@ public static (AccessResult result, uint minimum, uint current, uint maximum) Ge current: currentValue, maximum: maximumValue); } - var (message, errorCode) = Error.GetMessageCode(); + var (errorCode, message) = Error.GetCodeMessage(); Debug.WriteLine($"Failed to get brightnesses (Low level). {message}"); - return (result: GetResult(errorCode), 0, 0, 0); + return (result: new AccessResult(GetStatus(errorCode), $"Low level, {message}"), 0, 0, 0); } } @@ -414,9 +414,9 @@ public static (AccessResult result, uint minimum, uint current, uint maximum) Ge /// /// Physical monitor handle /// Raw brightness (not always 0 to 100) - /// Whether to use low level function + /// Whether to use high level function /// Result - public static AccessResult SetBrightness(SafePhysicalMonitorHandle physicalMonitorHandle, uint brightness, bool useLowLevel = false) + public static AccessResult SetBrightness(SafePhysicalMonitorHandle physicalMonitorHandle, uint brightness, bool useHighLevel = true) { if (physicalMonitorHandle is null) throw new ArgumentNullException(nameof(physicalMonitorHandle)); @@ -427,7 +427,7 @@ public static AccessResult SetBrightness(SafePhysicalMonitorHandle physicalMonit return AccessResult.Failed; } - if (!useLowLevel) + if (useHighLevel) { // SetMonitorBrightness function may return true even when it actually failed. if (SetMonitorBrightness( @@ -436,9 +436,9 @@ public static AccessResult SetBrightness(SafePhysicalMonitorHandle physicalMonit { return AccessResult.Succeeded; } - var (message, errorCode) = Error.GetMessageCode(); + var (errorCode, message) = Error.GetCodeMessage(); Debug.WriteLine($"Failed to set brightness (High level). {message}"); - return GetResult(errorCode); + return new AccessResult(GetStatus(errorCode), $"High level {message}"); } else { @@ -449,9 +449,9 @@ public static AccessResult SetBrightness(SafePhysicalMonitorHandle physicalMonit { return AccessResult.Succeeded; } - var (message, errorCode) = Error.GetMessageCode(); + var (errorCode, message) = Error.GetCodeMessage(); Debug.WriteLine($"Failed to set brightness (Low level). {message}"); - return GetResult(errorCode); + return new AccessResult(GetStatus(errorCode), $"Low level, {message}"); } } @@ -465,7 +465,7 @@ public static AccessResult SetBrightness(SafePhysicalMonitorHandle physicalMonit private const uint ERROR_GRAPHICS_DDCCI_INVALID_MESSAGE_CHECKSUM = 0xC026258B; private const uint ERROR_GRAPHICS_MONITOR_NO_LONGER_EXISTS = 0xC026258D; - private static AccessResult GetResult(int errorCode) + private static AccessStatus GetStatus(int errorCode) { return unchecked((uint)errorCode) switch { @@ -473,9 +473,9 @@ ERROR_GRAPHICS_DDCCI_VCP_NOT_SUPPORTED or ERROR_GRAPHICS_DDCCI_INVALID_DATA or ERROR_GRAPHICS_DDCCI_INVALID_MESSAGE_COMMAND or ERROR_GRAPHICS_DDCCI_INVALID_MESSAGE_LENGTH or - ERROR_GRAPHICS_DDCCI_INVALID_MESSAGE_CHECKSUM => AccessResult.DdcFailed, - ERROR_GRAPHICS_MONITOR_NO_LONGER_EXISTS => AccessResult.NoLongerExist, - _ => AccessResult.Failed + ERROR_GRAPHICS_DDCCI_INVALID_MESSAGE_CHECKSUM => AccessStatus.DdcFailed, + ERROR_GRAPHICS_MONITOR_NO_LONGER_EXISTS => AccessStatus.NoLongerExist, + _ => AccessStatus.Failed }; } diff --git a/Source/Monitorian.Core/Models/Monitor/MonitorManager.cs b/Source/Monitorian.Core/Models/Monitor/MonitorManager.cs index d5afbec9..f1cbeb59 100644 --- a/Source/Monitorian.Core/Models/Monitor/MonitorManager.cs +++ b/Source/Monitorian.Core/Models/Monitor/MonitorManager.cs @@ -11,7 +11,6 @@ using System.Threading.Tasks; using Monitorian.Core.Helper; -using Monitorian.Supplement; namespace Monitorian.Core.Models.Monitor { @@ -54,9 +53,9 @@ public static async Task> EnumerateMonitorsAsync() private static async Task> GetMonitorDevicesAsync() { - var displayItems = OsVersion.Is10Redstone4OrNewer - ? await DisplayInformation.GetDisplayMonitorsAsync() - : Array.Empty(); + IDisplayItem[] displayItems = OsVersion.Is10Redstone4OrNewer + ? await DisplayMonitor.GetDisplayMonitorsAsync() + : DisplayConfig.EnumerateDisplayConfigs().ToArray(); var deviceItems = DeviceContext.EnumerateMonitorDevices().ToArray(); _ids = new HashSet(deviceItems.Select(x => x.DeviceInstanceId)); @@ -123,7 +122,7 @@ private static IEnumerable EnumerateMonitors(List devi displayIndex: deviceItem.DisplayIndex, monitorIndex: deviceItem.MonitorIndex, handle: physicalItem.Handle, - useLowLevel: physicalItem.IsLowLevelSupported); + useHighLevel: physicalItem.IsHighLevelSupported); deviceItems.RemoveAt(index); if (deviceItems.Count == 0) @@ -132,35 +131,29 @@ private static IEnumerable EnumerateMonitors(List devi } // Obtained by WMI - var installedItems = DeviceInformation.EnumerateInstalledMonitors().ToArray(); - foreach (var desktopItem in MSMonitor.EnumerateDesktopMonitors()) { - foreach (var installedItem in installedItems) + int index = -1; + if (desktopItem.BrightnessLevels.Any()) { - int index = -1; - if (desktopItem.BrightnessLevels.Any()) - { - index = deviceItems.FindIndex(x => - string.Equals(x.DeviceInstanceId, desktopItem.DeviceInstanceId, StringComparison.OrdinalIgnoreCase) && - string.Equals(x.DeviceInstanceId, installedItem.DeviceInstanceId, StringComparison.OrdinalIgnoreCase)); - } - if (index < 0) - continue; + index = deviceItems.FindIndex(x => + string.Equals(x.DeviceInstanceId, desktopItem.DeviceInstanceId, StringComparison.OrdinalIgnoreCase)); + } + if (index < 0) + continue; - var deviceItem = deviceItems[index]; - yield return new WmiMonitorItem( - deviceInstanceId: deviceItem.DeviceInstanceId, - description: deviceItem.AlternateDescription, - displayIndex: deviceItem.DisplayIndex, - monitorIndex: deviceItem.MonitorIndex, - brightnessLevels: desktopItem.BrightnessLevels, - isRemovable: installedItem.IsRemovable); + var deviceItem = deviceItems[index]; + yield return new WmiMonitorItem( + deviceInstanceId: deviceItem.DeviceInstanceId, + description: deviceItem.AlternateDescription, + displayIndex: deviceItem.DisplayIndex, + monitorIndex: deviceItem.MonitorIndex, + isInternal: deviceItem.IsInternal, + brightnessLevels: desktopItem.BrightnessLevels); - deviceItems.RemoveAt(index); - if (deviceItems.Count == 0) - yield break; - } + deviceItems.RemoveAt(index); + if (deviceItems.Count == 0) + yield break; } // Unreachable neither by DDC/CI nor by WMI @@ -225,8 +218,8 @@ public PhysicalItemPlus( private void TestBrightness() { - var (getResult, minimum, current, maximum) = MonitorConfiguration.GetBrightness(Handle, IsLowLevelSupported); - var isGetSuccess = (getResult == AccessResult.Succeeded); + var (getResult, minimum, current, maximum) = MonitorConfiguration.GetBrightness(Handle, IsHighLevelSupported); + var isGetSuccess = (getResult.Status == AccessStatus.Succeeded); var isValid = (minimum < maximum) && (minimum <= current) && (current <= maximum); GetBrightness = $"Success: {isGetSuccess}" + (isGetSuccess ? $", Valid: {isValid} (Minimum: {minimum}, Current: {current}, Maximum: {maximum})" : string.Empty); @@ -238,13 +231,13 @@ private void TestBrightness() expected = Math.Min(maximum, Math.Max(minimum, expected)); } - var setResult = MonitorConfiguration.SetBrightness(Handle, expected, IsLowLevelSupported); - var isSetSuccess = (setResult == AccessResult.Succeeded); - var (_, _, actual, _) = MonitorConfiguration.GetBrightness(Handle, IsLowLevelSupported); + var setResult = MonitorConfiguration.SetBrightness(Handle, expected, IsHighLevelSupported); + var isSetSuccess = (setResult.Status == AccessStatus.Succeeded); + var (_, _, actual, _) = MonitorConfiguration.GetBrightness(Handle, IsHighLevelSupported); SetBrightness = $"Success: {isSetSuccess}" + (isSetSuccess ? $", Match: {expected == actual} (Expected: {expected}, Actual: {actual})" : string.Empty); if (isSetSuccess) - MonitorConfiguration.SetBrightness(Handle, current, IsLowLevelSupported); + MonitorConfiguration.SetBrightness(Handle, current, IsHighLevelSupported); } } @@ -260,20 +253,20 @@ private class MonitorData [DataMember(Order = 1, Name = "Device Context - DeviceItems")] public DeviceContext.DeviceItem[] DeviceItems { get; private set; } - [DataMember(Order = 2, Name = "Display Config - ConfigItems")] - public DisplayConfig.ConfigItem[] ConfigItems { get; private set; } + [DataMember(Order = 2, Name = "DisplayMonitor - DisplayItems")] + public DisplayMonitor.DisplayItem[] DisplayMonitorItems { get; private set; } - [DataMember(Order = 3, Name = "Monitor Configuration - PhysicalItems")] - public Dictionary PhysicalItems { get; private set; } + [DataMember(Order = 3, Name = "Display Config - DisplayItems")] + public DisplayConfig.DisplayItem[] DisplayConfigItems { get; private set; } [DataMember(Order = 4, Name = "Device Installation - InstalledItems")] public DeviceInformation.InstalledItem[] InstalledItems { get; private set; } - [DataMember(Order = 5, Name = "MSMonitorClass - DesktopItems")] - public MSMonitor.DesktopItem[] DesktopItems { get; private set; } + [DataMember(Order = 5, Name = "Monitor Configuration - PhysicalItems")] + public Dictionary PhysicalItems { get; private set; } - [DataMember(Order = 6, Name = "DisplayMonitor - DisplayItems")] - public DisplayInformation.DisplayItem[] DisplayItems { get; private set; } + [DataMember(Order = 6, Name = "MSMonitorClass - DesktopItems")] + public MSMonitor.DesktopItem[] DesktopItems { get; private set; } [DataMember(Order = 7)] public string[] ElapsedTime { get; private set; } @@ -292,8 +285,17 @@ public async Task PopulateAsync() GetTask(nameof(DeviceItems), () => DeviceItems = DeviceContext.EnumerateMonitorDevices().ToArray()), - GetTask(nameof(ConfigItems), () => - ConfigItems = DisplayConfig.EnumerateDisplayConfigs().ToArray()), + GetTask(nameof(DisplayMonitorItems), async () => + { + if (OsVersion.Is10Redstone4OrNewer) + DisplayMonitorItems = await DisplayMonitor.GetDisplayMonitorsAsync(); + }), + + GetTask(nameof(DisplayConfigItems), () => + DisplayConfigItems = DisplayConfig.EnumerateDisplayConfigs().ToArray()), + + GetTask(nameof(InstalledItems), () => + InstalledItems = DeviceInformation.EnumerateInstalledMonitors().ToArray()), GetTask(nameof(PhysicalItems), () => PhysicalItems = DeviceContext.GetMonitorHandles().ToDictionary( @@ -302,17 +304,8 @@ public async Task PopulateAsync() .Select(x => new PhysicalItemPlus(x)) .ToArray())), - GetTask(nameof(InstalledItems), () => - InstalledItems = DeviceInformation.EnumerateInstalledMonitors().ToArray()), - GetTask(nameof(DesktopItems), () => - DesktopItems = MSMonitor.EnumerateDesktopMonitors().ToArray()), - - GetTask(nameof(DisplayItems), async () => - { - if (OsVersion.Is10Redstone4OrNewer) - DisplayItems = await DisplayInformation.GetDisplayMonitorsAsync(); - }) + DesktopItems = MSMonitor.EnumerateDesktopMonitors().ToArray()) }; sw.Start(); diff --git a/Source/Monitorian.Core/Models/Monitor/UnreachableMonitorItem.cs b/Source/Monitorian.Core/Models/Monitor/UnreachableMonitorItem.cs index c527053c..8fb70c25 100644 --- a/Source/Monitorian.Core/Models/Monitor/UnreachableMonitorItem.cs +++ b/Source/Monitorian.Core/Models/Monitor/UnreachableMonitorItem.cs @@ -25,7 +25,7 @@ public UnreachableMonitorItem( this.IsInternal = isInternal; } - public override AccessResult UpdateBrightness(int brightness = -1) => default; - public override AccessResult SetBrightness(int brightness) => default; + public override AccessResult UpdateBrightness(int brightness = -1) => AccessResult.Failed; + public override AccessResult SetBrightness(int brightness) => AccessResult.Failed; } } \ No newline at end of file diff --git a/Source/Monitorian.Core/Models/Monitor/WmiMonitorItem.cs b/Source/Monitorian.Core/Models/Monitor/WmiMonitorItem.cs index d64e3b65..3c3a5380 100644 --- a/Source/Monitorian.Core/Models/Monitor/WmiMonitorItem.cs +++ b/Source/Monitorian.Core/Models/Monitor/WmiMonitorItem.cs @@ -13,35 +13,29 @@ namespace Monitorian.Core.Models.Monitor /// internal class WmiMonitorItem : MonitorItem { + private readonly bool _isInternal; private readonly byte[] _brightnessLevels; - private readonly bool _isRemovable; public WmiMonitorItem( string deviceInstanceId, string description, byte displayIndex, byte monitorIndex, - IEnumerable brightnessLevels, - bool isRemovable) : base( + bool isInternal, + IEnumerable brightnessLevels) : base( deviceInstanceId: deviceInstanceId, description: description, displayIndex: displayIndex, monitorIndex: monitorIndex, isReachable: true) { + this._isInternal = isInternal; this._brightnessLevels = brightnessLevels?.ToArray() ?? throw new ArgumentNullException(nameof(brightnessLevels)); - this._isRemovable = isRemovable; } public override AccessResult UpdateBrightness(int brightness = -1) { - if (_isRemovable) - { - this.Brightness = (0 <= brightness) - ? brightness - : MSMonitor.GetBrightness(DeviceInstanceId); - } - else + if (_isInternal) { this.Brightness = PowerManagement.GetActiveSchemeBrightness(); @@ -51,6 +45,12 @@ public override AccessResult UpdateBrightness(int brightness = -1) ? brightness : MSMonitor.GetBrightness(DeviceInstanceId); } + else + { + this.Brightness = (0 <= brightness) + ? brightness + : MSMonitor.GetBrightness(DeviceInstanceId); + } return (0 <= this.Brightness) ? AccessResult.Succeeded : AccessResult.Failed; } @@ -59,11 +59,9 @@ public override AccessResult SetBrightness(int brightness) if (brightness is < 0 or > 100) throw new ArgumentOutOfRangeException(nameof(brightness), brightness, "The brightness must be within 0 to 100."); - if (_isRemovable) + if (_isInternal) { - brightness = ArraySearch.GetNearest(_brightnessLevels, (byte)brightness); - - if (MSMonitor.SetBrightness(DeviceInstanceId, brightness)) + if (PowerManagement.SetActiveSchemeBrightness(brightness)) { this.Brightness = brightness; return AccessResult.Succeeded; @@ -71,7 +69,9 @@ public override AccessResult SetBrightness(int brightness) } else { - if (PowerManagement.SetActiveSchemeBrightness(brightness)) + brightness = ArraySearch.GetNearest(_brightnessLevels, (byte)brightness); + + if (MSMonitor.SetBrightness(DeviceInstanceId, brightness)) { this.Brightness = brightness; return AccessResult.Succeeded; diff --git a/Source/Monitorian.Core/Models/OperationRecorder.cs b/Source/Monitorian.Core/Models/OperationRecorder.cs index ae85f4d4..ab9182a0 100644 --- a/Source/Monitorian.Core/Models/OperationRecorder.cs +++ b/Source/Monitorian.Core/Models/OperationRecorder.cs @@ -11,20 +11,24 @@ namespace Monitorian.Core.Models { public class OperationRecorder { - /// - /// The number of entries that operation log file can contain - /// - public static int Capacity { get; set; } = 128; + private OperationRecorder() + { } - public OperationRecorder(string message) => LogService.RecordOperation(message, Capacity); + public static async Task CreateAsync(string message) + { + await LogService.PrepareOperationAsync(); + await LogService.RecordOperationAsync(message); + + return new(); + } - public void Record(string content) => LogService.RecordOperation(content, Capacity); + public Task RecordAsync(string content) => LogService.RecordOperationAsync(content); #region Line private readonly Lazy> _record = new(() => new( TimeSpan.FromSeconds(1), - async queue => await Task.Run(() => LogService.RecordOperation(string.Join(Environment.NewLine, queue), Capacity)))); + async queue => await LogService.RecordOperationAsync(string.Join(Environment.NewLine, queue)))); private readonly Lazy>> _actionLines = new(() => new()); @@ -76,7 +80,7 @@ public async Task EndGroupRecordAsync() { var groupsStrings = _actionGroups.Value.GroupBy(x => x.groupName).Select(x => (x.Key, (object)x.Select(y => y.item))).ToArray(); - await Task.Run(() => LogService.RecordOperation($"{_actionName}{Environment.NewLine}{SimpleSerialization.Serialize(groupsStrings)}", Capacity)); + await LogService.RecordOperationAsync($"{_actionName}{Environment.NewLine}{SimpleSerialization.Serialize(groupsStrings)}"); _actionName = null; _actionGroups.Value.Clear(); diff --git a/Source/Monitorian.Core/Models/SettingsCore.cs b/Source/Monitorian.Core/Models/SettingsCore.cs index c123ddf2..27cb8bfc 100644 --- a/Source/Monitorian.Core/Models/SettingsCore.cs +++ b/Source/Monitorian.Core/Models/SettingsCore.cs @@ -99,6 +99,8 @@ public bool MakesOperationLog #endregion + protected Type[] KnownTypes { get; set; } + private const string SettingsFileName = "settings.xml"; private readonly string _fileName; @@ -112,9 +114,9 @@ protected SettingsCore(string fileName) private Throttle _save; - protected internal virtual void Initiate() + protected internal virtual async Task InitiateAsync() { - Load(this); + await Task.Run(() => Load(this)); _save = new Throttle( TimeSpan.FromMilliseconds(100), @@ -130,7 +132,7 @@ private void Load(T instance) where T : class { try { - AppDataService.Load(instance, _fileName); + AppDataService.Load(instance, _fileName, KnownTypes); } catch (Exception ex) { @@ -143,7 +145,7 @@ private void Save(T instance) where T : class { try { - AppDataService.Save(instance, _fileName); + AppDataService.Save(instance, _fileName, KnownTypes); } catch (Exception ex) { diff --git a/Source/Monitorian.Core/Models/Watcher/PowerWatcher.cs b/Source/Monitorian.Core/Models/Watcher/PowerWatcher.cs index fa0ae7b0..291d6a29 100644 --- a/Source/Monitorian.Core/Models/Watcher/PowerWatcher.cs +++ b/Source/Monitorian.Core/Models/Watcher/PowerWatcher.cs @@ -41,7 +41,7 @@ public PowerWatcher() { // Conform invocation timings so that the action would be executed efficiently when // multiple and different events are fired almost simultaneously. - _resumeWatcher = new PowerModeWatcher(this, 5, 5, 10, 10, 30, 30); + _resumeWatcher = new PowerModeWatcher(this, 5, 5, 10, 10, 30); _statusWatcher = new PowerModeWatcher(this, 1, 4); } diff --git a/Source/Monitorian.Core/Models/Watcher/SystemEventsComplement.cs b/Source/Monitorian.Core/Models/Watcher/SystemEventsComplement.cs index 670f6de5..2dcc5c5e 100644 --- a/Source/Monitorian.Core/Models/Watcher/SystemEventsComplement.cs +++ b/Source/Monitorian.Core/Models/Watcher/SystemEventsComplement.cs @@ -152,6 +152,6 @@ public class PowerSettingChangedEventArgs : EventArgs public Guid Guid { get; } public int Data { get; } - public PowerSettingChangedEventArgs(Guid guid, int data) => (Guid, Data) = (guid, data); + public PowerSettingChangedEventArgs(Guid guid, int data) => (this.Guid, this.Data) = (guid, data); } } \ No newline at end of file diff --git a/Source/Monitorian.Core/Monitorian.Core.csproj b/Source/Monitorian.Core/Monitorian.Core.csproj index 6c486ec2..45b7c194 100644 --- a/Source/Monitorian.Core/Monitorian.Core.csproj +++ b/Source/Monitorian.Core/Monitorian.Core.csproj @@ -69,6 +69,7 @@ + diff --git a/Source/Monitorian.Core/Properties/AssemblyInfo.cs b/Source/Monitorian.Core/Properties/AssemblyInfo.cs index a2927dbc..8ce02d01 100644 --- a/Source/Monitorian.Core/Properties/AssemblyInfo.cs +++ b/Source/Monitorian.Core/Properties/AssemblyInfo.cs @@ -33,8 +33,8 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.13.3.0")] -[assembly: AssemblyFileVersion("2.13.3.0")] +[assembly: AssemblyVersion("2.14.0.0")] +[assembly: AssemblyFileVersion("2.14.0.0")] [assembly: NeutralResourcesLanguage("en-US")] // For unit test diff --git a/Source/Monitorian.Core/ViewModels/MonitorViewModel.cs b/Source/Monitorian.Core/ViewModels/MonitorViewModel.cs index 75eeec58..fe00d82d 100644 --- a/Source/Monitorian.Core/ViewModels/MonitorViewModel.cs +++ b/Source/Monitorian.Core/ViewModels/MonitorViewModel.cs @@ -28,17 +28,17 @@ public MonitorViewModel(AppControllerCore controller, IMonitor monitor) internal void Replace(IMonitor monitor) { - if (monitor is null) - return; - - lock (_lock) + if (monitor?.IsReachable is true) { - // If IsReachable property is changed to true, reset _controllableCount. - if (!this._monitor.IsReachable && monitor.IsReachable) - _controllableCount = InitialCount; - - this._monitor.Dispose(); - this._monitor = monitor; + lock (_lock) + { + this._monitor.Dispose(); + this._monitor = monitor; + } + } + else + { + monitor?.Dispose(); } } @@ -146,20 +146,24 @@ public bool UpdateBrightness(int brightness = -1) result = _monitor.UpdateBrightness(brightness); } - switch (result) + switch (result.Status) { - case AccessResult.Succeeded: + case AccessStatus.Succeeded: RaisePropertyChanged(nameof(BrightnessSystemChanged)); // This must be prior to Brightness. RaisePropertyChanged(nameof(Brightness)); RaisePropertyChanged(nameof(BrightnessSystemAdjusted)); OnSucceeded(); return true; - case AccessResult.NoLongerExist: - _controller.OnMonitorsChangeFound(); - goto default; - default: + _controller.OnMonitorAccessFailed(result); + + switch (result.Status) + { + case AccessStatus.NoLongerExist: + _controller.OnMonitorsChangeFound(); + break; + } OnFailed(); return false; } @@ -223,18 +227,23 @@ private bool SetBrightness(int brightness) result = _monitor.SetBrightness(brightness); } - switch (result) + switch (result.Status) { - case AccessResult.Succeeded: + case AccessStatus.Succeeded: RaisePropertyChanged(nameof(Brightness)); OnSucceeded(); return true; - case AccessResult.NoLongerExist: - _controller.OnMonitorsChangeFound(); - goto default; - default: + _controller.OnMonitorAccessFailed(result); + + switch (result.Status) + { + case AccessStatus.DdcFailed: + case AccessStatus.NoLongerExist: + _controller.OnMonitorsChangeFound(); + break; + } OnFailed(); return false; } @@ -244,10 +253,10 @@ private bool SetBrightness(int brightness) #region Controllable - public bool IsControllable => _monitor.IsReachable && (0 < _controllableCount); + public bool IsReachable => _monitor.IsReachable; - public bool IsLikelyControllable => IsControllable || _hasSucceeded; - private bool _hasSucceeded; + public bool IsControllable => IsReachable && ((0 < _controllableCount) || _isConfirmed); + private bool _isConfirmed; // This count is for determining IsControllable property. // To set this count, the following points need to be taken into account: @@ -257,13 +266,13 @@ private bool SetBrightness(int brightness) // - The initial count is intended to give allowance for failures before the first success. // If the count has been consumed without any success, the monitor will be regarded as // uncontrollable at all. - // - _hasSucceeded field indicates that the monitor has succeeded at least once. It will be + // - _isConfirmed field indicates that the monitor has succeeded at least once. It will be // set true at the first success and at a succeeding success after a failure. // - The normal count gives allowance for failures after the first and succeeding successes. // As long as the monitor continues to succeed, the count will stay at the normal count. // Each time the monitor fails, the count decreases. The decreased count will be reverted // to the normal count when the monitor succeeds again. - // - The initial count must be smaller than the normal count so that _hasSucceeded field + // - The initial count must be smaller than the normal count so that _isConfirmed field // will be set at the first success while reducing unnecessary access to the field. private short _controllableCount = InitialCount; private const short InitialCount = 3; @@ -278,10 +287,10 @@ private void OnSucceeded() if (formerCount <= 0) { RaisePropertyChanged(nameof(IsControllable)); - RaisePropertyChanged(nameof(Status)); + RaisePropertyChanged(nameof(Message)); } - _hasSucceeded = true; + _isConfirmed = true; } } @@ -290,23 +299,23 @@ private void OnFailed() if (--_controllableCount == 0) { RaisePropertyChanged(nameof(IsControllable)); - RaisePropertyChanged(nameof(Status)); + RaisePropertyChanged(nameof(Message)); } } - public string Status + public string Message { get { - if (IsControllable) + if (0 < _controllableCount) return null; LanguageService.Switch(); var reason = _monitor switch { - DdcMonitorItem _ => Resources.StatusReasonDdcFailing, - UnreachableMonitorItem { IsInternal: false } _ => Resources.StatusReasonDdcNotEnabled, + DdcMonitorItem => Resources.StatusReasonDdcFailing, + UnreachableMonitorItem { IsInternal: false } => Resources.StatusReasonDdcNotEnabled, _ => null, }; @@ -358,7 +367,7 @@ public override string ToString() (nameof(Name), Name), (nameof(IsUnison), IsUnison), (nameof(IsControllable), IsControllable), - (nameof(IsLikelyControllable), IsLikelyControllable), + ("IsConfirmed", _isConfirmed), ("ControllableCount", _controllableCount), (nameof(IsByKey), IsByKey), (nameof(IsSelected), IsSelected), diff --git a/Source/Monitorian.Core/ViewModels/ProbeSectionViewModel.cs b/Source/Monitorian.Core/ViewModels/ProbeSectionViewModel.cs index 11fce4eb..6d83e551 100644 --- a/Source/Monitorian.Core/ViewModels/ProbeSectionViewModel.cs +++ b/Source/Monitorian.Core/ViewModels/ProbeSectionViewModel.cs @@ -40,7 +40,7 @@ public void PerformProbe() public void PerformCopy() { - Task.Run(() => LogService.CopyOperation()); + Task.Run(() => LogService.CopyOperationAsync()); } public void PerformRescan() diff --git a/Source/Monitorian.Core/Views/MainWindow.xaml b/Source/Monitorian.Core/Views/MainWindow.xaml index 649918c7..77618e4a 100644 --- a/Source/Monitorian.Core/Views/MainWindow.xaml +++ b/Source/Monitorian.Core/Views/MainWindow.xaml @@ -581,6 +581,7 @@ + @@ -592,7 +593,6 @@ @@ -670,15 +670,15 @@ Visibility="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.Settings.ChangesRange, Converter={StaticResource BooleanToVisibilityConverterKey}}"/> - - - + + + Text="{Binding Message, Mode=OneWay}"/> diff --git a/Source/Monitorian.Supplement/DisplayInformation.cs b/Source/Monitorian.Supplement/DisplayInformation.cs index c92704ca..a3850f41 100644 --- a/Source/Monitorian.Supplement/DisplayInformation.cs +++ b/Source/Monitorian.Supplement/DisplayInformation.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; using Windows.Devices.Display; @@ -24,37 +23,31 @@ public class DisplayInformation /// /// Display monitor information /// - [DataContract] public class DisplayItem { /// /// Device ID (Not device interface ID) /// - [DataMember(Order = 0)] public string DeviceInstanceId { get; } /// /// Display name /// - [DataMember(Order = 1)] public string DisplayName { get; } /// /// Whether the display is connected internally /// - [DataMember(Order = 2)] public bool IsInternal { get; } /// /// Connection description /// - [DataMember(Order = 3)] public string ConnectionDescription { get; } /// /// Physical size (diagonal) in inches /// - [DataMember(Order = 4)] public float PhysicalSize { get; } internal DisplayItem( @@ -111,7 +104,6 @@ public static async Task GetDisplayMonitorsAsync() continue; //Debug.WriteLine($"DeviceInstanceId: {deviceInstanceId}"); - //Debug.WriteLine($"DeviceName: {device.Name}"); //Debug.WriteLine($"DisplayName: {displayMonitor.DisplayName}"); //Debug.WriteLine($"ConnectionKind: {displayMonitor.ConnectionKind}"); //Debug.WriteLine($"PhysicalConnector: {displayMonitor.PhysicalConnector}"); @@ -148,6 +140,9 @@ private static string GetConnectionDescription(DisplayMonitorConnectionKind conn case DisplayMonitorConnectionKind.Wired: switch (connectorKind) { + case DisplayMonitorPhysicalConnectorKind.HD15: + return "VGA"; + case DisplayMonitorPhysicalConnectorKind.AnalogTV: case DisplayMonitorPhysicalConnectorKind.DisplayPort: return connectorKind.ToString(); @@ -157,9 +152,6 @@ private static string GetConnectionDescription(DisplayMonitorConnectionKind conn case DisplayMonitorPhysicalConnectorKind.Lvds: case DisplayMonitorPhysicalConnectorKind.Sdi: return connectorKind.ToString().ToUpper(); - - case DisplayMonitorPhysicalConnectorKind.HD15: - return "VGA"; } break; } diff --git a/Source/Monitorian.Supplement/Monitorian.Supplement.csproj b/Source/Monitorian.Supplement/Monitorian.Supplement.csproj index 4948e6a2..0947c08d 100644 --- a/Source/Monitorian.Supplement/Monitorian.Supplement.csproj +++ b/Source/Monitorian.Supplement/Monitorian.Supplement.csproj @@ -41,7 +41,6 @@ - False $(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5\System.Runtime.WindowsRuntime.dll diff --git a/Source/Monitorian.Supplement/Properties/AssemblyInfo.cs b/Source/Monitorian.Supplement/Properties/AssemblyInfo.cs index 38bc5592..2da2a264 100644 --- a/Source/Monitorian.Supplement/Properties/AssemblyInfo.cs +++ b/Source/Monitorian.Supplement/Properties/AssemblyInfo.cs @@ -33,6 +33,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.13.0.0")] -[assembly: AssemblyFileVersion("2.13.0.0")] +[assembly: AssemblyVersion("2.14.0.0")] +[assembly: AssemblyFileVersion("2.14.0.0")] [assembly: NeutralResourcesLanguage("en-US")] diff --git a/Source/Monitorian.Supplement/UIInformation.cs b/Source/Monitorian.Supplement/UIInformation.cs index ecda68d9..0c856b52 100644 --- a/Source/Monitorian.Supplement/UIInformation.cs +++ b/Source/Monitorian.Supplement/UIInformation.cs @@ -49,8 +49,7 @@ public static bool IsLightThemeUsed() using var key = Registry.CurrentUser.OpenSubKey(keyName); - return (key?.GetValue(valueName) is int value) - && (value == 1); + return (key?.GetValue(valueName) is 1); } private static readonly object _lock = new object(); diff --git a/Source/Monitorian/Models/Settings.cs b/Source/Monitorian/Models/Settings.cs index 8c5a0ef6..5933a0f9 100644 --- a/Source/Monitorian/Models/Settings.cs +++ b/Source/Monitorian/Models/Settings.cs @@ -15,9 +15,9 @@ public class Settings : SettingsCore public Settings() : base(null) { } - protected override void Initiate() + protected override Task InitiateAsync() { - base.Initiate(); + return base.InitiateAsync(); } } } \ No newline at end of file diff --git a/Source/Monitorian/Properties/AssemblyInfo.cs b/Source/Monitorian/Properties/AssemblyInfo.cs index ee2c9412..c3050779 100644 --- a/Source/Monitorian/Properties/AssemblyInfo.cs +++ b/Source/Monitorian/Properties/AssemblyInfo.cs @@ -51,7 +51,7 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.13.3.0")] -[assembly: AssemblyFileVersion("2.13.3.0")] +[assembly: AssemblyVersion("2.14.0.0")] +[assembly: AssemblyFileVersion("2.14.0.0")] [assembly: Guid("a4cc5362-9b08-465b-ad64-5cfabc72a4c7")] [assembly: NeutralResourcesLanguage("en-US")] diff --git a/Source/ScreenFrame/Helper/CultureInfoAddition.cs b/Source/ScreenFrame/Helper/CultureInfoAddition.cs new file mode 100644 index 00000000..d2ffda3c --- /dev/null +++ b/Source/ScreenFrame/Helper/CultureInfoAddition.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace ScreenFrame.Helper +{ + /// + /// Additional methods for + /// + internal static class CultureInfoAddition + { + #region Win32 + + [DllImport("Kernel32.dll")] + private static extern ushort GetUserDefaultUILanguage(); + + #endregion + + public static CultureInfo UserDefaultUICulture => _userDefaultUICulture.Value; + private static readonly Lazy _userDefaultUICulture = new(() => new(GetUserDefaultUILanguage())); + } +} \ No newline at end of file diff --git a/Source/ScreenFrame/Movers/BasicWindowMover.cs b/Source/ScreenFrame/Movers/BasicWindowMover.cs index e7b3dba4..c48c903b 100644 --- a/Source/ScreenFrame/Movers/BasicWindowMover.cs +++ b/Source/ScreenFrame/Movers/BasicWindowMover.cs @@ -44,7 +44,7 @@ protected override void HandleWindowPosChanging(IntPtr hwnd, int msg, IntPtr wPa if ((0 < width) && (0 < height) && TryGetAdjacentLocation(width, height, out Rect adjacentLocation) && - TryConfirmLocation(adjacentLocation)) + TryConfirmLocation(ref adjacentLocation)) { position.x = (int)adjacentLocation.X; position.y = (int)adjacentLocation.Y; @@ -71,9 +71,7 @@ protected override void HandleWindowPosChanged(IntPtr hwnd, int msg, IntPtr wPar /// Handles Display change event. /// protected override void HandleDisplayChange(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) - { - _monitorRects = null; - } + { } /// /// Attempts to get the adjacent location using specified window width and height. @@ -84,8 +82,6 @@ protected override void HandleDisplayChange(IntPtr hwnd, int msg, IntPtr wParam, /// True if successfully gets protected abstract bool TryGetAdjacentLocation(double windowWidth, double windowHeight, out Rect location); - private static Rect[] _monitorRects; // Static field - /// /// Attempts to confirm that a specified location is not completely outside of monitors. /// @@ -94,18 +90,24 @@ protected override void HandleDisplayChange(IntPtr hwnd, int msg, IntPtr wParam, /// /// The specified location and the current location are not necessarily in the same monitor. /// - protected virtual bool TryConfirmLocation(Rect location) + protected virtual bool TryConfirmLocation(ref Rect location) { - return (_monitorRects ??= WindowHelper.GetMonitorRects()).Any(x => - { - // Rect.IntersectsWith method is not enough because it will return true when two rectangles - // share only the outline. - var intersection = Rect.Intersect(x, location); + if (!WindowHelper.TryGetMonitorRect(location, out Rect monitorRect, out _)) + return false; + + if (monitorRect.Contains(location)) + return true; + + // Rect.IntersectsWith method is not enough to check intersection because it will return + // true when two rectangles share only the outline. + // If intersection is Rect.Empty, its width and height will be double.NegativeInfinity. + if (Rect.Intersect(location, monitorRect).IsEmpty) + return false; - // If intersection is Rect.Empty, its width and height will be double.NegativeInfinity and - // its square will be double.PositiveInfinity. - return (intersection.Width > 0D) && (intersection.Height > 0D); - }); + var left = Math.Max(monitorRect.Left, Math.Min(monitorRect.Right, location.Right) - location.Width); + var top = Math.Max(monitorRect.Top, Math.Min(monitorRect.Bottom, location.Bottom) - location.Height); + location = new Rect(left, top, location.Width, location.Height); + return true; } } } \ No newline at end of file diff --git a/Source/ScreenFrame/Movers/FloatWindowMover.cs b/Source/ScreenFrame/Movers/FloatWindowMover.cs index 89c2b95d..1b6e66d8 100644 --- a/Source/ScreenFrame/Movers/FloatWindowMover.cs +++ b/Source/ScreenFrame/Movers/FloatWindowMover.cs @@ -5,6 +5,8 @@ using System.Threading.Tasks; using System.Windows; +using ScreenFrame.Helper; + namespace ScreenFrame.Movers { /// @@ -54,31 +56,39 @@ protected bool TryGetAdjacentLocationToPivot(double windowWidth, double windowHe return false; } + var isLeftToRight = !CultureInfoAddition.UserDefaultUICulture.TextInfo.IsRightToLeft; + + PivotAlignment = (taskbarAlignment, isLeftToRight) switch + { + (TaskbarAlignment.Top, true) => PivotAlignment.TopRight, + (TaskbarAlignment.Top, false) => PivotAlignment.TopLeft, + (TaskbarAlignment.Bottom, true) => PivotAlignment.BottomRight, + (TaskbarAlignment.Bottom, false) => PivotAlignment.BottomLeft, + (TaskbarAlignment.Left, { }) => PivotAlignment.BottomLeft, + (TaskbarAlignment.Right, { }) => PivotAlignment.BottomRight, + _ => default + }; + var x = _pivot.X; var y = _pivot.Y; - switch (taskbarAlignment) + switch (PivotAlignment) { - case TaskbarAlignment.Left: - // Place this window at the top-right of the pivot. + case PivotAlignment.TopLeft: x += 1; - y += -windowHeight - 1; - PivotAlignment = PivotAlignment.BottomLeft; + y += 1; break; - - case TaskbarAlignment.Top: - // Place this window at the bottom-left of the pivot. - x += -windowWidth - 1; + case PivotAlignment.TopRight: + x -= (windowWidth + 1); y += 1; - PivotAlignment = PivotAlignment.TopRight; break; - - case TaskbarAlignment.Right: - case TaskbarAlignment.Bottom: - // Place this window at the top-left of the pivot. - x += -windowWidth - 1; - y += -windowHeight - 1; - PivotAlignment = PivotAlignment.BottomRight; + case PivotAlignment.BottomLeft: + x += 1; + y -= (windowHeight + 1); + break; + case PivotAlignment.BottomRight: + x -= (windowWidth + 1); + y -= (windowHeight + 1); break; } location = new Rect(x, y, windowWidth, windowHeight); diff --git a/Source/ScreenFrame/Movers/StickWindowMover.cs b/Source/ScreenFrame/Movers/StickWindowMover.cs index 858a2311..9fc17970 100644 --- a/Source/ScreenFrame/Movers/StickWindowMover.cs +++ b/Source/ScreenFrame/Movers/StickWindowMover.cs @@ -6,6 +6,8 @@ using System.Windows; using System.Windows.Forms; +using ScreenFrame.Helper; + namespace ScreenFrame.Movers { /// @@ -78,11 +80,14 @@ protected bool TryGetAdjacentLocationToTaskbar(double windowWidth, double window if (NotifyIconHelper.TryGetNotifyIconRect(_notifyIcon, out Rect iconRect)) { - if (taskbarRect.Contains(iconRect)) + if (taskbarRect.Contains( + iconRect.X + iconRect.Width / 2D, + iconRect.Y + iconRect.Height / 2D)) { iconPlacement = IconPlacement.InTaskbar; } - else if (WindowHelper.TryGetOverflowAreaRect(out overflowAreaRect)) + else if (WindowHelper.TryGetOverflowAreaRect(out overflowAreaRect) + && overflowAreaRect.Contains(iconRect)) { iconPlacement = IconPlacement.InOverflowArea; } @@ -91,6 +96,8 @@ protected bool TryGetAdjacentLocationToTaskbar(double windowWidth, double window if (!WindowHelper.TryGetDwmWindowMargin(_window, out Thickness windowMargin)) windowMargin = new Thickness(0); // Fallback + var isLeftToRight = !CultureInfoAddition.UserDefaultUICulture.TextInfo.IsRightToLeft; + double x = 0, y = 0; switch (taskbarAlignment) @@ -99,21 +106,21 @@ protected bool TryGetAdjacentLocationToTaskbar(double windowWidth, double window case TaskbarAlignment.Bottom: x = iconPlacement switch { - IconPlacement.InTaskbar => iconRect.Right, - IconPlacement.InOverflowArea => overflowAreaRect.Left, - _ => taskbarRect.Right,// Fallback + IconPlacement.InTaskbar => isLeftToRight ? iconRect.Right : iconRect.Left, + IconPlacement.InOverflowArea => isLeftToRight ? overflowAreaRect.Left : overflowAreaRect.Right, + _ => isLeftToRight ? taskbarRect.Right : taskbarRect.Left, // Fallback }; - x -= (windowWidth - windowMargin.Right); + x -= isLeftToRight ? (windowWidth - windowMargin.Right) : windowMargin.Left; switch (taskbarAlignment) { case TaskbarAlignment.Top: y = taskbarRect.Bottom - windowMargin.Top; - PivotAlignment = PivotAlignment.TopRight; + PivotAlignment = isLeftToRight ? PivotAlignment.TopRight : PivotAlignment.TopLeft; break; case TaskbarAlignment.Bottom: y = taskbarRect.Top - (windowHeight - windowMargin.Bottom); - PivotAlignment = PivotAlignment.BottomRight; + PivotAlignment = isLeftToRight ? PivotAlignment.BottomRight : PivotAlignment.BottomLeft; break; } break; @@ -135,7 +142,7 @@ protected bool TryGetAdjacentLocationToTaskbar(double windowWidth, double window { IconPlacement.InTaskbar => iconRect.Bottom, IconPlacement.InOverflowArea => overflowAreaRect.Top, - _ => taskbarRect.Bottom,// Fallback + _ => taskbarRect.Bottom, // Fallback }; y -= (windowHeight - windowMargin.Bottom); break; diff --git a/Source/ScreenFrame/Properties/AssemblyInfo.cs b/Source/ScreenFrame/Properties/AssemblyInfo.cs index 10248444..aae732c6 100644 --- a/Source/ScreenFrame/Properties/AssemblyInfo.cs +++ b/Source/ScreenFrame/Properties/AssemblyInfo.cs @@ -33,6 +33,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.13.3.0")] -[assembly: AssemblyFileVersion("2.13.3.0")] +[assembly: AssemblyVersion("2.14.0.0")] +[assembly: AssemblyFileVersion("2.14.0.0")] [assembly: NeutralResourcesLanguage("en-US")] diff --git a/Source/ScreenFrame/ScreenFrame.csproj b/Source/ScreenFrame/ScreenFrame.csproj index 49dd6101..f07b47dd 100644 --- a/Source/ScreenFrame/ScreenFrame.csproj +++ b/Source/ScreenFrame/ScreenFrame.csproj @@ -48,6 +48,7 @@ + diff --git a/Source/ScreenFrame/WindowHelper.cs b/Source/ScreenFrame/WindowHelper.cs index 645254be..cb92ec76 100644 --- a/Source/ScreenFrame/WindowHelper.cs +++ b/Source/ScreenFrame/WindowHelper.cs @@ -259,7 +259,7 @@ private static extern int GetClassName( #region Monitor - public static bool TryGetMonitorRect(Rect windowRect, out Rect monitorRect) + public static bool TryGetMonitorRect(Rect windowRect, out Rect monitorRect, out Rect workRect) { RECT rect = windowRect; var monitorHandle = MonitorFromRect( @@ -274,10 +274,12 @@ public static bool TryGetMonitorRect(Rect windowRect, out Rect monitorRect) ref monitorInfo)) { monitorRect = monitorInfo.rcMonitor; + workRect = monitorInfo.rcWork; return true; } } monitorRect = Rect.Empty; + workRect = Rect.Empty; return false; } @@ -305,8 +307,8 @@ bool Proc(IntPtr monitorHandle, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dw { if (Convert.ToBoolean(monitorInfo.dwFlags & MONITORINFOF_PRIMARY)) { - // Store the primary monitor at the beginning of the collection because in most cases, - // the primary monitor should be checked first. + // Store the primary monitor at the beginning of the collection because in + // most cases, the primary monitor should be checked first. monitorRects.Insert(0, monitorInfo.rcMonitor); } else @@ -542,6 +544,9 @@ private static TaskbarAlignment GetTaskbarAlignment(Rect monitorRect, Rect taskb public static bool TryGetOverflowAreaRect(out Rect overflowAreaRect) { + // This combination of functions will not produce current location of overflow area + // until it is shown in the monitor where primary taskbar currently locates. Thus + // the location must be verified by other means. var overflowAreaHandle = FindWindowEx( IntPtr.Zero, IntPtr.Zero,