From 86cd7fe3e46fa638cc8208235e8807926a0edfc2 Mon Sep 17 00:00:00 2001 From: Xjph Date: Fri, 7 Jul 2023 08:36:27 -0230 Subject: [PATCH] WIP: observatory UI overhaul --- .vscode/launch.json | 4 +- .../ObservatoryBotanist.csproj | 1 + .../NativeNotification/NativePopup.cs | 31 +- ObservatoryCore/ObservatoryCore.cs | 7 +- ObservatoryCore/ObservatoryCore.csproj | 5 +- .../PluginManagement/PluginCore.cs | 18 +- .../PluginManagement/PluginManager.cs | 63 ++-- ObservatoryCore/UI/CoreForm.Designer.cs | 2 + ObservatoryCore/UI/CoreForm.Plugins.cs | 109 ++++++ ObservatoryCore/UI/CoreForm.Settings.cs | 111 +++++++ ObservatoryCore/UI/CoreForm.cs | 217 ++---------- ObservatoryCore/UI/DefaultSorter.cs | 64 +++- ObservatoryCore/UI/DwmHelper.cs | 310 +++++++++++++++++ .../UI/NotificationForm.Designer.cs | 52 ++- ObservatoryCore/UI/NotificationForm.cs | 204 +++++++++++- ObservatoryCore/UI/NotificationForm.resx | 62 +--- ObservatoryCore/UI/PluginHelper.cs | 87 +++-- ObservatoryCore/UI/SettingsPanel.cs | 312 ++++++++++++++++-- ObservatoryCore/Utils/LogMonitor.cs | 2 +- ObservatoryCore/Utils/SettingsManager.cs | 67 ++++ ObservatoryDev/ObservatoryDev.sln | 15 +- ObservatoryExplorer/DefaultCriteria.cs | 4 + .../ObservatoryExplorer.csproj | 9 +- ObservatoryFramework/Interfaces.cs | 36 +- .../ObservatoryFramework.csproj | 5 + ObservatoryFramework/ObservatoryFramework.xml | 28 +- ObservatoryFramework/PluginUI.cs | 8 +- ObservatoryHerald/ObservatoryHerald.csproj | 1 + 28 files changed, 1448 insertions(+), 386 deletions(-) create mode 100644 ObservatoryCore/UI/CoreForm.Plugins.cs create mode 100644 ObservatoryCore/UI/CoreForm.Settings.cs create mode 100644 ObservatoryCore/UI/DwmHelper.cs create mode 100644 ObservatoryCore/Utils/SettingsManager.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index ebce6d8..9926df6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,12 +5,12 @@ // Use IntelliSense to find out which attributes exist for C# debugging // Use hover for the description of the existing attributes // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md - "name": ".NET Core Launch (console)", + "name": ".NET Core Launch (debug)", "type": "coreclr", "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/ObservatoryCore/bin/Debug/net5.0/ObservatoryCore.dll", + "program": "${workspaceFolder}/ObservatoryCore/bin/debug/net6.0-windows/ObservatoryCore.exe", "args": [], "cwd": "${workspaceFolder}/ObservatoryCore", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console diff --git a/ObservatoryBotanist/ObservatoryBotanist.csproj b/ObservatoryBotanist/ObservatoryBotanist.csproj index e89d165..b7dcb13 100644 --- a/ObservatoryBotanist/ObservatoryBotanist.csproj +++ b/ObservatoryBotanist/ObservatoryBotanist.csproj @@ -5,6 +5,7 @@ enable false ObservatoryKey.snk + Debug;Release;Portable diff --git a/ObservatoryCore/NativeNotification/NativePopup.cs b/ObservatoryCore/NativeNotification/NativePopup.cs index 2f4aed7..eff794e 100644 --- a/ObservatoryCore/NativeNotification/NativePopup.cs +++ b/ObservatoryCore/NativeNotification/NativePopup.cs @@ -15,25 +15,29 @@ namespace Observatory.NativeNotification public Guid InvokeNativeNotification(NotificationArgs notificationArgs) { var notificationGuid = Guid.NewGuid(); - var notification = new NotificationForm() + Application.OpenForms[0].Invoke(() => { - Guid = notificationGuid - }; - notification.Show(); - notifications.Add(notificationGuid, notification); - - //TODO: Implement winform notification + var notification = new NotificationForm(notificationGuid, notificationArgs); + notification.FormClosed += NotifyWindow_Closed; + + notifications.Add(notificationGuid, notification); + notification.Show(); + }); + return notificationGuid; } - private void NotifyWindow_Closed(object sender, EventArgs e) + private void NotifyWindow_Closed(object? sender, EventArgs e) { - var currentNotification = (NotificationForm)sender; - - if (notifications.ContainsKey(currentNotification.Guid)) + if (sender != null) { - notifications.Remove(currentNotification.Guid); + var currentNotification = (NotificationForm)sender; + + if (notifications.ContainsKey(currentNotification.Guid)) + { + notifications.Remove(currentNotification.Guid); + } } } @@ -49,8 +53,7 @@ namespace Observatory.NativeNotification { if (notifications.ContainsKey(guid)) { - //TODO: Update notification content - // notifications[guid].DataContext = new NotificationViewModel(notificationArgs); + notifications[guid].Update(notificationArgs); } } diff --git a/ObservatoryCore/ObservatoryCore.cs b/ObservatoryCore/ObservatoryCore.cs index f3f69cf..4abc1fc 100644 --- a/ObservatoryCore/ObservatoryCore.cs +++ b/ObservatoryCore/ObservatoryCore.cs @@ -1,4 +1,5 @@ using Observatory.PluginManagement; +using Observatory.Utils; using System.Reflection.PortableExecutable; namespace Observatory @@ -11,6 +12,8 @@ namespace Observatory [STAThread] static void Main(string[] args) { + SettingsManager.Load(); + if (args.Length > 0 && File.Exists(args[0])) { var fileInfo = new FileInfo(args[0]); @@ -27,14 +30,14 @@ namespace Observatory { try { - Properties.Core.Default.Upgrade(); + // Properties.Core.Default.Upgrade(); } catch { // Silently ignore properties upgrade failure. } Properties.Core.Default.CoreVersion = version; - Properties.Core.Default.Save(); + SettingsManager.Save(); } diff --git a/ObservatoryCore/ObservatoryCore.csproj b/ObservatoryCore/ObservatoryCore.csproj index ae449bf..6bd0e88 100644 --- a/ObservatoryCore/ObservatoryCore.csproj +++ b/ObservatoryCore/ObservatoryCore.csproj @@ -7,6 +7,7 @@ true enable Observatory + Debug;Release;Portable @@ -59,9 +60,9 @@ - + - + diff --git a/ObservatoryCore/PluginManagement/PluginCore.cs b/ObservatoryCore/PluginManagement/PluginCore.cs index 6e696bb..97773c2 100644 --- a/ObservatoryCore/PluginManagement/PluginCore.cs +++ b/ObservatoryCore/PluginManagement/PluginCore.cs @@ -105,17 +105,22 @@ namespace Observatory.PluginManagement public void AddGridItems(IObservatoryWorker worker, IEnumerable items) { - //TODO: Add to winform list + //TODO: Use better bulk handling here. + foreach (var item in items) + { + worker.PluginUI.DataGrid.Add(item); + } } public void ClearGrid(IObservatoryWorker worker, object templateItem) { - //TODO: Clear winform list + worker.PluginUI.DataGrid.Clear(); } public void ExecuteOnUIThread(Action action) { - //TODO: Execute action + if (Application.OpenForms.Count > 0) + Application.OpenForms[0].Invoke(action); } public System.Net.Http.HttpClient HttpClient @@ -140,7 +145,7 @@ namespace Observatory.PluginManagement get { var context = new System.Diagnostics.StackFrame(1).GetMethod(); - +#if DEBUG || RELEASE string folderLocation = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + $"{Path.DirectorySeparatorChar}ObservatoryCore{Path.DirectorySeparatorChar}{context?.DeclaringType?.Assembly.GetName().Name}{Path.DirectorySeparatorChar}"; @@ -148,6 +153,11 @@ namespace Observatory.PluginManagement Directory.CreateDirectory(folderLocation); return folderLocation; +#elif PORTABLE + string? observatoryLocation = System.Diagnostics.Process.GetCurrentProcess()?.MainModule?.FileName; + var obsDir = new FileInfo(observatoryLocation ?? String.Empty).DirectoryName; + return $"{obsDir}{Path.DirectorySeparatorChar}plugins{Path.DirectorySeparatorChar}{context?.DeclaringType?.Assembly.GetName().Name}-Data{Path.DirectorySeparatorChar}"; +#endif } } diff --git a/ObservatoryCore/PluginManagement/PluginManager.cs b/ObservatoryCore/PluginManagement/PluginManager.cs index 59cfa73..2df6441 100644 --- a/ObservatoryCore/PluginManagement/PluginManager.cs +++ b/ObservatoryCore/PluginManagement/PluginManager.cs @@ -30,7 +30,7 @@ namespace Observatory.PluginManagement } - public readonly List<(string error, string detail)> errorList; + public readonly List<(string error, string? detail)> errorList; public readonly List pluginPanels; public readonly List pluginTables; public readonly List<(IObservatoryWorker plugin, PluginStatus signed)> workerPlugins; @@ -181,21 +181,22 @@ namespace Observatory.PluginManagement }); Properties.Core.Default.PluginSettings = newSettings; - Properties.Core.Default.Save(); + SettingsManager.Save(); } - private static List<(string, string)> LoadPlugins(out List<(IObservatoryWorker plugin, PluginStatus signed)> observatoryWorkers, out List<(IObservatoryNotifier plugin, PluginStatus signed)> observatoryNotifiers) + private static List<(string, string?)> LoadPlugins(out List<(IObservatoryWorker plugin, PluginStatus signed)> observatoryWorkers, out List<(IObservatoryNotifier plugin, PluginStatus signed)> observatoryNotifiers) { observatoryWorkers = new(); observatoryNotifiers = new(); - var errorList = new List<(string, string)>(); + var errorList = new List<(string, string?)>(); string pluginPath = $"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins"; - - string ownExe = System.Reflection.Assembly.GetExecutingAssembly().Location; + + string? ownExe = System.Diagnostics.Process.GetCurrentProcess()?.MainModule?.FileName; FileSignatureInfo ownSig; - using (var stream = File.OpenRead(ownExe)) + // This will throw if ownExe is null, but that's an error condition regardless. + using (var stream = File.OpenRead(ownExe ?? String.Empty)) ownSig = FileSignatureInfo.GetFromFileStream(stream); @@ -259,7 +260,7 @@ namespace Observatory.PluginManagement if (response == DialogResult.OK) { Properties.Core.Default.UnsignedAllowed.Add(pluginHash); - Properties.Core.Default.Save(); + SettingsManager.Save(); } else { @@ -322,16 +323,16 @@ namespace Observatory.PluginManagement System.Runtime.Loader.AssemblyLoadContext.Default.Resolving += (context, name) => { - if (name.Name.EndsWith("resources")) + if ((name?.Name?.EndsWith("resources")).GetValueOrDefault(false)) { return null; } // Importing Observatory.Framework in the Explorer Lua scripts causes an attempt to reload // the assembly, just hand it back the one we already have. - if (name.Name.StartsWith("Observatory.Framework") || name.Name == "ObservatoryFramework") + if ((name?.Name?.StartsWith("Observatory.Framework")).GetValueOrDefault(false) || name?.Name == "ObservatoryFramework") { - return context.Assemblies.Where(a => a.FullName.Contains("ObservatoryFramework")).First(); + return context.Assemblies.Where(a => (a.FullName?.Contains("ObservatoryFramework")).GetValueOrDefault(false)).First(); } var foundDlls = Directory.GetFileSystemEntries(new FileInfo($"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins{Path.DirectorySeparatorChar}deps").FullName, name.Name + ".dll", SearchOption.TopDirectoryOnly); @@ -340,7 +341,7 @@ namespace Observatory.PluginManagement return context.LoadFromAssemblyPath(foundDlls[0]); } - if (name.Name != recursionGuard) + if (name.Name != recursionGuard && name.Name != null) { recursionGuard = name.Name; return context.LoadFromAssemblyName(name); @@ -361,37 +362,43 @@ namespace Observatory.PluginManagement } catch (ReflectionTypeLoadException ex) { - types = ex.Types.Where(t => t != null).ToArray(); + types = ex.Types.OfType().ToArray(); } catch { types = Array.Empty(); } - var workerTypes = types.Where(t => t.IsAssignableTo(typeof(IObservatoryWorker))); - foreach (var worker in workerTypes) + IEnumerable workerTypes = types.Where(t => t.IsAssignableTo(typeof(IObservatoryWorker))); + foreach (Type worker in workerTypes) { - ConstructorInfo constructor = worker.GetConstructor(Array.Empty()); - object instance = constructor.Invoke(Array.Empty()); - workers.Add((instance as IObservatoryWorker, pluginStatus)); - if (instance is IObservatoryNotifier) + ConstructorInfo? constructor = worker.GetConstructor(Array.Empty()); + if (constructor != null) { - // This is also a notifier; add to the notifier list as well, so the work and notifier are - // the same instance and can share state. - notifiers.Add((instance as IObservatoryNotifier, pluginStatus)); + object instance = constructor.Invoke(Array.Empty()); + workers.Add(((instance as IObservatoryWorker)!, pluginStatus)); + if (instance is IObservatoryNotifier) + { + // This is also a notifier; add to the notifier list as well, so the work and notifier are + // the same instance and can share state. + notifiers.Add(((instance as IObservatoryNotifier)!, pluginStatus)); + } + pluginCount++; } - pluginCount++; } // Filter out items which are also workers as we've already created them above. var notifyTypes = types.Where(t => t.IsAssignableTo(typeof(IObservatoryNotifier)) && !t.IsAssignableTo(typeof(IObservatoryWorker))); - foreach (var notifier in notifyTypes) + foreach (Type notifier in notifyTypes) { - ConstructorInfo constructor = notifier.GetConstructor(Array.Empty()); - object instance = constructor.Invoke(Array.Empty()); - notifiers.Add((instance as IObservatoryNotifier, PluginStatus.Signed)); - pluginCount++; + ConstructorInfo? constructor = notifier.GetConstructor(Array.Empty()); + if (constructor != null) + { + object instance = constructor.Invoke(Array.Empty()); + notifiers.Add(((instance as IObservatoryNotifier)!, PluginStatus.Signed)); + pluginCount++; + } } if (pluginCount == 0) diff --git a/ObservatoryCore/UI/CoreForm.Designer.cs b/ObservatoryCore/UI/CoreForm.Designer.cs index 785e87f..5258cbf 100644 --- a/ObservatoryCore/UI/CoreForm.Designer.cs +++ b/ObservatoryCore/UI/CoreForm.Designer.cs @@ -360,6 +360,7 @@ this.TestButton.TabIndex = 12; this.TestButton.Text = "Test"; this.TestButton.UseVisualStyleBackColor = false; + this.TestButton.Click += new System.EventHandler(this.TestButton_Click); // // ColourButton // @@ -569,6 +570,7 @@ this.ToggleMonitorButton.TabIndex = 3; this.ToggleMonitorButton.Text = "Start Monitor"; this.ToggleMonitorButton.UseVisualStyleBackColor = false; + this.ToggleMonitorButton.Click += new System.EventHandler(this.ToggleMonitorButton_Click); // // ClearButton // diff --git a/ObservatoryCore/UI/CoreForm.Plugins.cs b/ObservatoryCore/UI/CoreForm.Plugins.cs new file mode 100644 index 0000000..6b76cfa --- /dev/null +++ b/ObservatoryCore/UI/CoreForm.Plugins.cs @@ -0,0 +1,109 @@ +using Observatory.PluginManagement; +using Observatory.Framework.Interfaces; + +namespace Observatory.UI +{ + partial class CoreForm + { + + private void PopulatePluginList() + { + List uniquePlugins = new(); + + foreach (var (plugin, signed) in PluginManager.GetInstance.workerPlugins) + { + if (!uniquePlugins.Contains(plugin)) + { + uniquePlugins.Add(plugin); + ListViewItem item = new ListViewItem(new[] { plugin.Name, "Worker", plugin.Version, PluginStatusString(signed) }); + PluginList.Items.Add(item); + } + } + + foreach (var (plugin, signed) in PluginManager.GetInstance.notifyPlugins) + { + if (!uniquePlugins.Contains(plugin)) + { + uniquePlugins.Add(plugin); + ListViewItem item = new ListViewItem(new[] { plugin.Name, "Notifier", plugin.Version, PluginStatusString(signed) }); + PluginList.Items.Add(item); + } + } + } + + private static string PluginStatusString(PluginManager.PluginStatus status) + { + switch (status) + { + case PluginManager.PluginStatus.Signed: + return "Signed"; + + case PluginManager.PluginStatus.Unsigned: + return "Unsigned"; + + case PluginManager.PluginStatus.InvalidSignature: + return "Invalid Signature"; + + case PluginManager.PluginStatus.InvalidPlugin: + return "Invalid Plugin"; + + case PluginManager.PluginStatus.InvalidLibrary: + return "Invalid File"; + + case PluginManager.PluginStatus.NoCert: + return "Unsigned Observatory (Debug build)"; + + case PluginManager.PluginStatus.SigCheckDisabled: + return "Signature Checks Disabled"; + + default: + return string.Empty; + } + } + + private void CreatePluginTabs() + { + var uiPlugins = PluginManager.GetInstance.workerPlugins.Where(p => p.plugin.PluginUI.PluginUIType != Framework.PluginUI.UIType.None); + + PluginHelper.CreatePluginTabs(CoreMenu, uiPlugins, uiPanels); + + foreach(ToolStripMenuItem item in CoreMenu.Items) + { + pluginList.Add(item.Text, item); + } + } + + private void CreatePluginSettings() + { + foreach (var plugin in PluginManager.GetInstance.workerPlugins) + { + var pluginSettingsPanel = new SettingsPanel(plugin.plugin, AdjustPanelsBelow); + AddSettingsPanel(pluginSettingsPanel); + } + foreach (var plugin in PluginManager.GetInstance.notifyPlugins) + { + var pluginSettingsPanel = new SettingsPanel(plugin.plugin, AdjustPanelsBelow); + AddSettingsPanel(pluginSettingsPanel); + } + } + + private void AddSettingsPanel(SettingsPanel panel) + { + int lowestPoint = 0; + foreach (Control control in CorePanel.Controls) + { + if (control.Location.Y + control.Height > lowestPoint) + lowestPoint = control.Location.Y + control.Height; + } + DuplicateControlVisuals(PopupNotificationLabel, panel.Header); + panel.Header.TextAlign = PopupNotificationLabel.TextAlign; + panel.Header.Location = new Point(PopupNotificationLabel.Location.X, lowestPoint); + + DuplicateControlVisuals(PopupSettingsPanel, panel, false); + panel.Location = new Point(PopupSettingsPanel.Location.X, lowestPoint + panel.Header.Height); + panel.Visible = false; + CorePanel.Controls.Add(panel.Header); + CorePanel.Controls.Add(panel); + } + } +} \ No newline at end of file diff --git a/ObservatoryCore/UI/CoreForm.Settings.cs b/ObservatoryCore/UI/CoreForm.Settings.cs new file mode 100644 index 0000000..180877e --- /dev/null +++ b/ObservatoryCore/UI/CoreForm.Settings.cs @@ -0,0 +1,111 @@ +using Observatory.Utils; + +namespace Observatory.UI +{ + partial class CoreForm + { + private void ColourButton_Click(object _, EventArgs e) + { + var selectionResult = PopupColour.ShowDialog(); + if (selectionResult == DialogResult.OK) + { + ColourButton.BackColor = PopupColour.Color; + Properties.Core.Default.NativeNotifyColour = (uint)PopupColour.Color.ToArgb(); + SettingsManager.Save(); + } + } + + private void PopupCheckbox_CheckedChanged(object _, EventArgs e) + { + Properties.Core.Default.NativeNotify = PopupCheckbox.Checked; + SettingsManager.Save(); + } + + private void DurationSpinner_ValueChanged(object _, EventArgs e) + { + Properties.Core.Default.NativeNotifyTimeout = (int)DurationSpinner.Value; + SettingsManager.Save(); + } + + private void ScaleSpinner_ValueChanged(object _, EventArgs e) + { + Properties.Core.Default.NativeNotifyScale = (int)ScaleSpinner.Value; + SettingsManager.Save(); + } + + private void FontDropdown_SelectedIndexChanged(object _, EventArgs e) + { + Properties.Core.Default.NativeNotifyFont = FontDropdown.SelectedItem.ToString(); + SettingsManager.Save(); + } + + private void CornerDropdown_SelectedIndexChanged(object _, EventArgs e) + { + Properties.Core.Default.NativeNotifyCorner = CornerDropdown.SelectedIndex; + SettingsManager.Save(); + } + + private void DisplayDropdown_SelectedIndexChanged(object _, EventArgs e) + { + Properties.Core.Default.NativeNotifyScreen = DisplayDropdown.SelectedIndex - 1; + SettingsManager.Save(); + } + + private void VoiceVolumeSlider_Scroll(object _, EventArgs e) + { + Properties.Core.Default.VoiceVolume = VoiceVolumeSlider.Value; + SettingsManager.Save(); + } + + private void VoiceSpeedSlider_Scroll(object _, EventArgs e) + { + Properties.Core.Default.VoiceRate = VoiceSpeedSlider.Value; + SettingsManager.Save(); + } + + private void VoiceCheckbox_CheckedChanged(object _, EventArgs e) + { + Properties.Core.Default.VoiceNotify = VoiceCheckbox.Checked; + SettingsManager.Save(); + } + + private void VoiceDropdown_SelectedIndexChanged(object _, EventArgs e) + { + Properties.Core.Default.VoiceSelected = VoiceDropdown.SelectedItem.ToString(); + SettingsManager.Save(); + } + + private void PopulateDropdownOptions() + { + var fonts = new System.Drawing.Text.InstalledFontCollection().Families; + FontDropdown.Items.AddRange(fonts.Select(f => f.Name).ToArray()); + + DisplayDropdown.Items.Add("Primary"); + if (Screen.AllScreens.Length > 1) + for (int i = 0; i < Screen.AllScreens.Length; i++) + DisplayDropdown.Items.Add((i + 1).ToString()); + + var voices = new System.Speech.Synthesis.SpeechSynthesizer().GetInstalledVoices(); + foreach (var voice in voices.Select(v => v.VoiceInfo.Name)) + VoiceDropdown.Items.Add(voice); + + } + + private void PopulateNativeSettings() + { + var settings = Properties.Core.Default; + + DisplayDropdown.SelectedIndex = settings.NativeNotifyScreen + 1; + CornerDropdown.SelectedIndex = settings.NativeNotifyCorner; + FontDropdown.SelectedItem = settings.NativeNotifyFont; + ScaleSpinner.Value = settings.NativeNotifyScale; + DurationSpinner.Value = settings.NativeNotifyTimeout; + ColourButton.BackColor = Color.FromArgb((int)settings.NativeNotifyColour); + PopupCheckbox.Checked = settings.NativeNotify; + VoiceVolumeSlider.Value = settings.VoiceVolume; + VoiceSpeedSlider.Value = settings.VoiceRate; + VoiceDropdown.SelectedItem = settings.VoiceSelected; + VoiceCheckbox.Checked = settings.VoiceNotify; + } + } +} \ No newline at end of file diff --git a/ObservatoryCore/UI/CoreForm.cs b/ObservatoryCore/UI/CoreForm.cs index e3da322..03aefe9 100644 --- a/ObservatoryCore/UI/CoreForm.cs +++ b/ObservatoryCore/UI/CoreForm.cs @@ -1,6 +1,9 @@ -using Observatory.Framework.Interfaces; +using Observatory.Framework; +using Observatory.Framework.Interfaces; using Observatory.PluginManagement; using Observatory.Utils; +using System.Text; +using System.Windows.Forms; namespace Observatory.UI { @@ -37,40 +40,6 @@ namespace Observatory.UI AdjustPanelsBelow(PopupSettingsPanel, AdjustmentDirection.Up); } - private void PopulateDropdownOptions() - { - var fonts = new System.Drawing.Text.InstalledFontCollection().Families; - FontDropdown.Items.AddRange(fonts.Select(f => f.Name).ToArray()); - - DisplayDropdown.Items.Add("Primary"); - if (Screen.AllScreens.Length > 1) - for (int i = 0; i < Screen.AllScreens.Length; i++) - DisplayDropdown.Items.Add((i + 1).ToString()); - - var voices = new System.Speech.Synthesis.SpeechSynthesizer().GetInstalledVoices(); - foreach (var voice in voices.Select(v => v.VoiceInfo.Name)) - VoiceDropdown.Items.Add(voice); - - } - - private void PopulateNativeSettings() - { - var settings = Properties.Core.Default; - - DisplayDropdown.SelectedIndex = settings.NativeNotifyScreen + 1; - CornerDropdown.SelectedIndex = settings.NativeNotifyCorner; - FontDropdown.SelectedItem = settings.NativeNotifyFont; - ScaleSpinner.Value = settings.NativeNotifyScale; - DurationSpinner.Value = settings.NativeNotifyTimeout; - ColourButton.BackColor = Color.FromArgb((int)settings.NativeNotifyColour); - PopupCheckbox.Checked = settings.NativeNotify; - VoiceVolumeSlider.Value = settings.VoiceVolume; - VoiceSpeedSlider.Value = settings.VoiceRate; - VoiceDropdown.SelectedItem = settings.VoiceSelected; - VoiceCheckbox.Checked = settings.VoiceNotify; - } - - private void CoreMenu_SizeChanged(object? sender, EventArgs e) { CorePanel.Location = new Point(12 + CoreMenu.Width, 12); @@ -80,95 +49,27 @@ namespace Observatory.UI private Dictionary pluginList; - private void CreatePluginTabs() + private static void DuplicateControlVisuals(Control source, Control target, bool applyHeight = true) { - var uiPlugins = PluginManager.GetInstance.workerPlugins.Where(p => p.plugin.PluginUI.PluginUIType != Framework.PluginUI.UIType.None); - - PluginHelper.CreatePluginTabs(CoreMenu, uiPlugins, uiPanels); - - foreach(ToolStripMenuItem item in CoreMenu.Items) - { - pluginList.Add(item.Text, item); - } + if (applyHeight) target.Height = source.Height; + target.Width = source.Width; + target.Font = source.Font; + target.ForeColor = source.ForeColor; + target.BackColor = source.BackColor; + target.Anchor = source.Anchor; } - private void CreatePluginSettings() + private void ToggleMonitorButton_Click(object sender, EventArgs e) { - foreach (var plugin in PluginManager.GetInstance.workerPlugins) + if ((LogMonitor.GetInstance.CurrentState & Framework.LogMonitorState.Realtime) == Framework.LogMonitorState.Realtime) { - var pluginSettingsPanel = new SettingsPanel(plugin.plugin, AdjustPanelsBelow); - AddSettingsPanel(pluginSettingsPanel); + LogMonitor.GetInstance.Stop(); + ToggleMonitorButton.Text = "Start Monitor"; } - foreach (var plugin in PluginManager.GetInstance.notifyPlugins) + else { - var pluginSettingsPanel = new SettingsPanel(plugin.plugin, AdjustPanelsBelow); - AddSettingsPanel(pluginSettingsPanel); - } - } - - private void AddSettingsPanel(SettingsPanel panel) - { - int lowestPoint = 0; - foreach (Control control in CorePanel.Controls) - { - if (control.Location.Y + control.Height > lowestPoint) - lowestPoint = control.Location.Y + control.Height; - } - panel.Header.Location = new Point(PopupNotificationLabel.Location.X, lowestPoint); - panel.Header.Width = PopupNotificationLabel.Width; - panel.Header.Font = PopupNotificationLabel.Font; - panel.Header.ForeColor = PopupNotificationLabel.ForeColor; - panel.Header.BackColor = PopupNotificationLabel.BackColor; - panel.Header.TextAlign = PopupNotificationLabel.TextAlign; - panel.Location = new Point(PopupNotificationLabel.Location.X, lowestPoint + panel.Header.Height); - panel.Width = PopupSettingsPanel.Width; - CorePanel.Controls.Add(panel.Header); - CorePanel.Controls.Add(panel); - } - - private void PopulatePluginList() - { - List uniquePlugins = new(); - - - foreach (var (plugin, signed) in PluginManager.GetInstance.workerPlugins) - { - if (!uniquePlugins.Contains(plugin)) - { - uniquePlugins.Add(plugin); - ListViewItem item = new ListViewItem(new[] { plugin.Name, "Worker", plugin.Version, PluginStatusString(signed) }); - PluginList.Items.Add(item); - } - } - } - - private static string PluginStatusString(PluginManager.PluginStatus status) - { - switch (status) - { - case PluginManager.PluginStatus.Signed: - return "Signed"; - - case PluginManager.PluginStatus.Unsigned: - return "Unsigned"; - - case PluginManager.PluginStatus.InvalidSignature: - return "Invalid Signature"; - - case PluginManager.PluginStatus.InvalidPlugin: - return "Invalid Plugin"; - - case PluginManager.PluginStatus.InvalidLibrary: - return "Invalid File"; - - case PluginManager.PluginStatus.NoCert: - return "Unsigned Observatory (Debug build)"; - - case PluginManager.PluginStatus.SigCheckDisabled: - return "Signature Checks Disabled"; - - default: - return string.Empty; + LogMonitor.GetInstance.Start(); + ToggleMonitorButton.Text = "Stop Monitor"; } } @@ -337,82 +238,16 @@ namespace Observatory.UI Up, Down } - #region Settings Changes - - private void ColourButton_Click(object _, EventArgs e) + private void TestButton_Click(object sender, EventArgs e) { - var selectionResult = PopupColour.ShowDialog(); - if (selectionResult == DialogResult.OK) + NotificationArgs args = new() { - ColourButton.BackColor = PopupColour.Color; - Properties.Core.Default.NativeNotifyColour = (uint)PopupColour.Color.ToArgb(); - Properties.Core.Default.Save(); - } + Title = "Test Notification", + Detail = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec at elit maximus, ornare dui nec, accumsan velit. Vestibulum fringilla elit." + }; + var testNotify = new NotificationForm(new Guid(), args); + testNotify.Show(); + } - - private void PopupCheckbox_CheckedChanged(object _, EventArgs e) - { - Properties.Core.Default.NativeNotify = PopupCheckbox.Checked; - Properties.Core.Default.Save(); - } - - private void DurationSpinner_ValueChanged(object _, EventArgs e) - { - Properties.Core.Default.NativeNotifyTimeout = (int)DurationSpinner.Value; - Properties.Core.Default.Save(); - } - - private void ScaleSpinner_ValueChanged(object _, EventArgs e) - { - Properties.Core.Default.NativeNotifyScale = (int)ScaleSpinner.Value; - Properties.Core.Default.Save(); - } - - private void FontDropdown_SelectedIndexChanged(object _, EventArgs e) - { - Properties.Core.Default.NativeNotifyFont = FontDropdown.SelectedItem.ToString(); - Properties.Core.Default.Save(); - } - - private void CornerDropdown_SelectedIndexChanged(object _, EventArgs e) - { - Properties.Core.Default.NativeNotifyCorner = CornerDropdown.SelectedIndex; - Properties.Core.Default.Save(); - } - - private void DisplayDropdown_SelectedIndexChanged(object _, EventArgs e) - { - Properties.Core.Default.NativeNotifyScreen = DisplayDropdown.SelectedIndex - 1; - Properties.Core.Default.Save(); - } - - private void VoiceVolumeSlider_Scroll(object _, EventArgs e) - { - Properties.Core.Default.VoiceVolume = VoiceVolumeSlider.Value; - Properties.Core.Default.Save(); - } - - private void VoiceSpeedSlider_Scroll(object _, EventArgs e) - { - Properties.Core.Default.VoiceRate = VoiceSpeedSlider.Value; - Properties.Core.Default.Save(); - } - - private void VoiceCheckbox_CheckedChanged(object _, EventArgs e) - { - Properties.Core.Default.VoiceNotify = VoiceCheckbox.Checked; - Properties.Core.Default.Save(); - } - - private void VoiceDropdown_SelectedIndexChanged(object _, EventArgs e) - { - Properties.Core.Default.VoiceSelected = VoiceDropdown.SelectedItem.ToString(); - Properties.Core.Default.Save(); - } - - - #endregion - - } } \ No newline at end of file diff --git a/ObservatoryCore/UI/DefaultSorter.cs b/ObservatoryCore/UI/DefaultSorter.cs index 040859d..be77f8a 100644 --- a/ObservatoryCore/UI/DefaultSorter.cs +++ b/ObservatoryCore/UI/DefaultSorter.cs @@ -3,10 +3,11 @@ using System.Collections; using System.Linq; using System.Text; using System.Threading.Tasks; +using Observatory.Framework.Interfaces; namespace Observatory.UI { - internal class DefaultSorter : IComparer + internal class DefaultSorter : IObservatoryComparer { /// /// Specifies the column to be sorted @@ -15,7 +16,7 @@ namespace Observatory.UI /// /// Specifies the order in which to sort (i.e. 'Ascending'). /// - private SortOrder OrderOfSort; + private int OrderOfSort; /// /// Case insensitive comparer object /// @@ -30,7 +31,7 @@ namespace Observatory.UI ColumnToSort = 0; // Initialize the sort order to 'none' - OrderOfSort = SortOrder.None; + OrderOfSort = 0; // Initialize the CaseInsensitiveComparer object ObjectCompare = new CaseInsensitiveComparer(); @@ -49,25 +50,68 @@ namespace Observatory.UI ListViewItem? listviewX = (ListViewItem?)x; ListViewItem? listviewY = (ListViewItem?)y; + if (OrderOfSort == 0) + return 0; + // Compare the two items - compareResult = ObjectCompare.Compare(listviewX?.SubItems[ColumnToSort].Text, listviewY?.SubItems[ColumnToSort].Text); + compareResult = NaturalCompare(listviewX?.SubItems[ColumnToSort].Text, listviewY?.SubItems[ColumnToSort].Text); // Calculate correct return value based on object comparison - if (OrderOfSort == SortOrder.Ascending) + if (OrderOfSort == 1) { // Ascending sort is selected, return normal result of compare operation return compareResult; } - else if (OrderOfSort == SortOrder.Descending) + else { // Descending sort is selected, return negative result of compare operation return (-compareResult); } - else + } + + private static int NaturalCompare(string? x, string? y) + { + for (int i = 0; i <= x?.Length && i <= y?.Length; i++) { - // Return '0' to indicate they are equal - return 0; + // If we've reached the end of the string without finding a difference + // the longer string is "greater". + if (i == x.Length || i == y.Length) + return x.Length > y.Length ? 1 : y.Length > x.Length ? -1 : 0; + + // We've found a number in the same place in both strings. + if (Char.IsDigit(x[i]) && Char.IsDigit(y[i])) + { + // Walk ahead and get the full numbers. + string xNum = new(x[i..].TakeWhile(c => Char.IsDigit(c)).ToArray()); + string yNum = new(y[i..].TakeWhile(c => Char.IsDigit(c)).ToArray()); + + // Pad with zeroes to equal lengths. + int numLength = Math.Max(xNum.Length, yNum.Length); + string xNumPadded = xNum.PadLeft(numLength, '0'); + string yNumPadded = yNum.PadLeft(numLength, '0'); + + // Now that they're the same length a direct compare works. + int result = xNumPadded.CompareTo(yNumPadded); + if (result != 0) + { + return result; + } + else + { + // The numbers are identical, skip them and keep moving. + i += numLength - 1; + } + } + // Check if we have unequal letters. + else if (x[i] != y[i]) + { + // Straight compare and return. + return x[i] > y[i] ? 1 : -1; + } } + + // If we somehow make it here, return equal result. + return 0; } /// @@ -88,7 +132,7 @@ namespace Observatory.UI /// /// Gets or sets the order of sorting to apply (for example, 'Ascending' or 'Descending'). /// - public SortOrder Order + public int Order { set { diff --git a/ObservatoryCore/UI/DwmHelper.cs b/ObservatoryCore/UI/DwmHelper.cs new file mode 100644 index 0000000..5724088 --- /dev/null +++ b/ObservatoryCore/UI/DwmHelper.cs @@ -0,0 +1,310 @@ +// Source: https://stackoverflow.com/questions/51578104/how-to-create-a-semi-transparent-or-blurred-backcolor-in-a-windows-form + +using System.Runtime.InteropServices; +using System.Security; + +[SuppressUnmanagedCodeSecurity] +public class DwmHelper +{ + public const int WM_DWMCOMPOSITIONCHANGED = 0x031E; + + public struct MARGINS + { + public int leftWidth; + public int rightWidth; + public int topHeight; + public int bottomHeight; + + public MARGINS(int LeftWidth, int RightWidth, int TopHeight, int BottomHeight) + { + leftWidth = LeftWidth; + rightWidth = RightWidth; + topHeight = TopHeight; + bottomHeight = BottomHeight; + } + + public void NoMargins() + { + leftWidth = 0; + rightWidth = 0; + topHeight = 0; + bottomHeight = 0; + } + + public void SheetOfGlass() + { + leftWidth = -1; + rightWidth = -1; + topHeight = -1; + bottomHeight = -1; + } + } + + [Flags] + public enum DWM_BB + { + Enable = 1, + BlurRegion = 2, + TransitionOnMaximized = 4 + } + + // https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute + public enum DWMWINDOWATTRIBUTE : uint + { + NCRenderingEnabled = 1, //Get atttribute + NCRenderingPolicy, //Enable or disable non-client rendering + TransitionsForceDisabled, + AllowNCPaint, + CaptionButtonBounds, //Get atttribute + NonClientRtlLayout, + ForceIconicRepresentation, + Flip3DPolicy, + ExtendedFrameBounds, //Get atttribute + HasIconicBitmap, + DisallowPeek, + ExcludedFromPeek, + Cloak, + Cloaked, //Get atttribute. Returns a DWMCLOACKEDREASON + FreezeRepresentation, + PassiveUpdateMode, + UseHostBackDropBrush, + AccentPolicy = 19, // Win 10 (undocumented) + ImmersiveDarkMode = 20, // Win 11 22000 + WindowCornerPreference = 33, // Win 11 22000 + BorderColor, // Win 11 22000 + CaptionColor, // Win 11 22000 + TextColor, // Win 11 22000 + VisibleFrameBorderThickness, // Win 11 22000 + SystemBackdropType // Win 11 22621 + } + + public enum DWMCLOACKEDREASON : uint + { + DWM_CLOAKED_APP = 0x0000001, //cloaked by its owner application. + DWM_CLOAKED_SHELL = 0x0000002, //cloaked by the Shell. + DWM_CLOAKED_INHERITED = 0x0000004 //inherited from its owner window. + } + + public enum DWMNCRENDERINGPOLICY : uint + { + UseWindowStyle, // Enable/disable non-client rendering based on window style + Disabled, // Disabled non-client rendering; window style is ignored + Enabled, // Enabled non-client rendering; window style is ignored + }; + + public enum DWMACCENTSTATE + { + ACCENT_DISABLED = 0, + ACCENT_ENABLE_GRADIENT = 1, + ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, + ACCENT_ENABLE_BLURBEHIND = 3, + ACCENT_INVALID_STATE = 4 + } + + [Flags] + public enum CompositionAction : uint + { + DWM_EC_DISABLECOMPOSITION = 0, + DWM_EC_ENABLECOMPOSITION = 1 + } + + // Values designating how Flip3D treats a given window. + enum DWMFLIP3DWINDOWPOLICY : uint + { + Default, // Hide or include the window in Flip3D based on window style and visibility. + ExcludeBelow, // Display the window under Flip3D and disabled. + ExcludeAbove, // Display the window above Flip3D and enabled. + }; + + public enum ThumbProperties_dwFlags : uint + { + RectDestination = 0x00000001, + RectSource = 0x00000002, + Opacity = 0x00000004, + Visible = 0x00000008, + SourceClientAreaOnly = 0x00000010 + } + + + [StructLayout(LayoutKind.Sequential)] + public struct AccentPolicy + { + public DWMACCENTSTATE AccentState; + public int AccentFlags; + public int GradientColor; + public int AnimationId; + + public AccentPolicy(DWMACCENTSTATE accentState, int accentFlags, int gradientColor, int animationId) + { + AccentState = accentState; + AccentFlags = accentFlags; + GradientColor = gradientColor; + AnimationId = animationId; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct DWM_BLURBEHIND + { + public DWM_BB dwFlags; + public int fEnable; + public IntPtr hRgnBlur; + public int fTransitionOnMaximized; + + public DWM_BLURBEHIND(bool enabled) + { + dwFlags = DWM_BB.Enable; + fEnable = (enabled) ? 1 : 0; + hRgnBlur = IntPtr.Zero; + fTransitionOnMaximized = 0; + } + + public Region Region => Region.FromHrgn(hRgnBlur); + + public bool TransitionOnMaximized + { + get => fTransitionOnMaximized > 0; + set + { + fTransitionOnMaximized = (value) ? 1 : 0; + dwFlags |= DWM_BB.TransitionOnMaximized; + } + } + + public void SetRegion(Graphics graphics, Region region) + { + hRgnBlur = region.GetHrgn(graphics); + dwFlags |= DWM_BB.BlurRegion; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct WinCompositionAttrData + { + public DWMWINDOWATTRIBUTE Attribute; + public IntPtr Data; //Will point to an AccentPolicy struct, where Attribute will be DWMWINDOWATTRIBUTE.AccentPolicy + public int SizeOfData; + + public WinCompositionAttrData(DWMWINDOWATTRIBUTE attribute, IntPtr data, int sizeOfData) + { + Attribute = attribute; + Data = data; + SizeOfData = sizeOfData; + } + } + + private static int GetBlurBehindPolicyAccentFlags() + { + int drawLeftBorder = 20; + int drawTopBorder = 40; + int drawRightBorder = 80; + int drawBottomBorder = 100; + return (drawLeftBorder | drawTopBorder | drawRightBorder | drawBottomBorder); + } + + //https://msdn.microsoft.com/en-us/library/windows/desktop/aa969508(v=vs.85).aspx + [DllImport("dwmapi.dll")] + internal static extern int DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind); + + [DllImport("dwmapi.dll", PreserveSig = false)] + public static extern void DwmEnableComposition(CompositionAction uCompositionAction); + + //https://msdn.microsoft.com/it-it/library/windows/desktop/aa969512(v=vs.85).aspx + [DllImport("dwmapi.dll")] + internal static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS pMarInset); + + //https://msdn.microsoft.com/en-us/library/windows/desktop/aa969515(v=vs.85).aspx + [DllImport("dwmapi.dll")] + internal static extern int DwmGetWindowAttribute(IntPtr hwnd, DWMWINDOWATTRIBUTE attr, ref int attrValue, int attrSize); + + //https://msdn.microsoft.com/en-us/library/windows/desktop/aa969524(v=vs.85).aspx + [DllImport("dwmapi.dll")] + internal static extern int DwmSetWindowAttribute(IntPtr hwnd, DWMWINDOWATTRIBUTE attr, ref int attrValue, int attrSize); + + [DllImport("User32.dll", SetLastError = true)] + internal static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WinCompositionAttrData data); + + [DllImport("dwmapi.dll")] + internal static extern int DwmIsCompositionEnabled(ref int pfEnabled); + + public static bool IsCompositionEnabled() + { + int pfEnabled = 0; + int result = DwmIsCompositionEnabled(ref pfEnabled); + return (pfEnabled == 1) ? true : false; + } + + public static bool IsNonClientRenderingEnabled(IntPtr hWnd) + { + int gwaEnabled = 0; + int result = DwmGetWindowAttribute(hWnd, DWMWINDOWATTRIBUTE.NCRenderingEnabled, ref gwaEnabled, sizeof(int)); + return gwaEnabled == 1; + } + + public static bool WindowSetAttribute(IntPtr hWnd, DWMWINDOWATTRIBUTE attribute, int attributeValue) + { + int result = DwmSetWindowAttribute(hWnd, attribute, ref attributeValue, sizeof(int)); + return (result == 0); + } + + public static void Windows10EnableBlurBehind(IntPtr hWnd) + { + DWMNCRENDERINGPOLICY policy = DWMNCRENDERINGPOLICY.Enabled; + WindowSetAttribute(hWnd, DWMWINDOWATTRIBUTE.NCRenderingPolicy, (int)policy); + + AccentPolicy accPolicy = new AccentPolicy() + { + AccentState = DWMACCENTSTATE.ACCENT_ENABLE_BLURBEHIND, + }; + + int accentSize = Marshal.SizeOf(accPolicy); + IntPtr accentPtr = Marshal.AllocHGlobal(accentSize); + Marshal.StructureToPtr(accPolicy, accentPtr, false); + var data = new WinCompositionAttrData(DWMWINDOWATTRIBUTE.AccentPolicy, accentPtr, accentSize); + + SetWindowCompositionAttribute(hWnd, ref data); + Marshal.FreeHGlobal(accentPtr); + } + + public static bool WindowEnableBlurBehind(IntPtr hWnd) + { + DWMNCRENDERINGPOLICY policy = DWMNCRENDERINGPOLICY.Enabled; + WindowSetAttribute(hWnd, DWMWINDOWATTRIBUTE.NCRenderingPolicy, (int)policy); + + DWM_BLURBEHIND dwm_BB = new DWM_BLURBEHIND(true); + int result = DwmEnableBlurBehindWindow(hWnd, ref dwm_BB); + return result == 0; + } + + public static bool WindowExtendIntoClientArea(IntPtr hWnd, MARGINS margins) + { + // Extend frame on the bottom of client area + int result = DwmExtendFrameIntoClientArea(hWnd, ref margins); + return result == 0; + } + + public static bool WindowBorderlessDropShadow(IntPtr hWnd, int shadowSize) + { + MARGINS margins = new MARGINS(0, shadowSize, 0, shadowSize); + int result = DwmExtendFrameIntoClientArea(hWnd, ref margins); + return result == 0; + } + + public static bool WindowSheetOfGlass(IntPtr hWnd) + { + MARGINS margins = new MARGINS(); + + //Margins set to All:-1 - Sheet Of Glass effect + margins.SheetOfGlass(); + int result = DwmExtendFrameIntoClientArea(hWnd, ref margins); + return result == 0; + } + + public static bool WindowDisableRendering(IntPtr hWnd) + { + int ncrp = (int)DWMNCRENDERINGPOLICY.Disabled; + // Disable non-client area rendering on the window. + int result = DwmSetWindowAttribute(hWnd, DWMWINDOWATTRIBUTE.NCRenderingPolicy, ref ncrp, sizeof(int)); + return result == 0; + } +} \ No newline at end of file diff --git a/ObservatoryCore/UI/NotificationForm.Designer.cs b/ObservatoryCore/UI/NotificationForm.Designer.cs index 012e455..444350b 100644 --- a/ObservatoryCore/UI/NotificationForm.Designer.cs +++ b/ObservatoryCore/UI/NotificationForm.Designer.cs @@ -28,12 +28,60 @@ /// private void InitializeComponent() { - this.components = new System.ComponentModel.Container(); + this.Title = new System.Windows.Forms.Label(); + this.Body = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // Title + // + this.Title.Font = new System.Drawing.Font("Segoe UI", 24F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.Title.ForeColor = System.Drawing.Color.OrangeRed; + this.Title.Location = new System.Drawing.Point(5, 5); + this.Title.MaximumSize = new System.Drawing.Size(355, 0); + this.Title.Name = "Title"; + this.Title.Size = new System.Drawing.Size(338, 45); + this.Title.TabIndex = 0; + this.Title.Text = "Title"; + // + // Body + // + this.Body.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.Body.AutoSize = true; + this.Body.Font = new System.Drawing.Font("Segoe UI", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.Body.ForeColor = System.Drawing.Color.OrangeRed; + this.Body.Location = new System.Drawing.Point(12, 45); + this.Body.MaximumSize = new System.Drawing.Size(320, 85); + this.Body.Name = "Body"; + this.Body.Size = new System.Drawing.Size(51, 31); + this.Body.TabIndex = 1; + this.Body.Text = "Body"; + this.Body.UseCompatibleTextRendering = true; + // + // NotificationForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(800, 450); + this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(64)))), ((int)(((byte)(64))))); + this.ClientSize = new System.Drawing.Size(355, 145); + this.ControlBox = false; + this.Controls.Add(this.Body); + this.Controls.Add(this.Title); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "NotificationForm"; + this.ShowIcon = false; + this.ShowInTaskbar = false; this.Text = "NotificationForm"; + this.ResumeLayout(false); + this.PerformLayout(); + } #endregion + + private Label Title; + private Label Body; } } \ No newline at end of file diff --git a/ObservatoryCore/UI/NotificationForm.cs b/ObservatoryCore/UI/NotificationForm.cs index 26c5ef2..dee4e2e 100644 --- a/ObservatoryCore/UI/NotificationForm.cs +++ b/ObservatoryCore/UI/NotificationForm.cs @@ -1,9 +1,12 @@ -using System; +using Observatory.Framework; +using Observatory.Framework.Files.Journal; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; @@ -12,11 +15,206 @@ namespace Observatory.UI { public partial class NotificationForm : Form { - public NotificationForm() + private Color _color; + private readonly Guid _guid; + private readonly System.Timers.Timer _timer; + private bool _defaultPosition = true; + private Point _originalLocation; + + protected override bool ShowWithoutActivation => true; + protected override CreateParams CreateParams + { + get + { + CreateParams cp = base.CreateParams; + cp.ExStyle |= 0x00000008; // WS_EX_TOPMOST + return cp; + } + } + + public NotificationForm(Guid guid, NotificationArgs args) { + _guid = guid; + _color = Color.FromArgb((int)Properties.Core.Default.NativeNotifyColour); InitializeComponent(); + + Title.Paint += DrawText; + Body.Paint += DrawText; + + if (System.Environment.OSVersion.Version.Major >= 6 && DwmHelper.IsCompositionEnabled()) + { + if (Environment.OSVersion.Version.Major > 6) + { + DwmHelper.Windows10EnableBlurBehind(Handle); + } + else + { + DwmHelper.WindowEnableBlurBehind(Handle); + } + + // For some reason this causes the window to become all white on my own + // PC. Looks very similar to strange system-specific all-white behaviour + // of Avalonia. + // DwmHelper.WindowBorderlessDropShadow(Handle, 2); + } + + + Title.ForeColor = _color; + Title.Text = args.Title; + Title.Font = new Font(Properties.Core.Default.NativeNotifyFont, 24); + Body.ForeColor = _color; + Body.Text = args.Detail; + Body.Font = new Font(Properties.Core.Default.NativeNotifyFont, 14); + this.Paint += DrawBorder; + + AdjustPosition(args.XPos / 100, args.YPos / 100); + + _timer = new(); + _timer.Elapsed += CloseNotification; + if (args.Timeout != 0) + { + _timer.Interval = args.Timeout == -1 ? Properties.Core.Default.NativeNotifyTimeout : args.Timeout; + _timer.Start(); + } } - public Guid Guid; + public void Update(NotificationArgs notificationArgs) + { + Title.Text = notificationArgs.Title; + Body.Text = notificationArgs.Detail; + } + + private void AdjustPosition(double x = -1.0, double y = -1.0) + { + int screen = Properties.Core.Default.NativeNotifyScreen; + int corner = Properties.Core.Default.NativeNotifyCorner; + Rectangle screenBounds; + + + if (screen == -1 || screen > Screen.AllScreens.Length) + if (Screen.AllScreens.Length == 1) + screenBounds = Screen.GetBounds(this); + else + screenBounds = Screen.PrimaryScreen.Bounds; + else + screenBounds = Screen.AllScreens[screen - 1].Bounds; + + if (x >= 0 && y >= 0) + { + _defaultPosition = false; + int xLocation = Convert.ToInt32(screenBounds.Width * x); + int yLocation = Convert.ToInt32(screenBounds.Height * x); + Location = Point.Add(screenBounds.Location, new Size(xLocation, yLocation)); + } + else + { + _defaultPosition = true; + switch (corner) + { + default: + case 0: + Location = Point.Add( + new Point(screenBounds.Right, screenBounds.Bottom), + new Size(-(Width+50), -(Height+50))); + break; + case 1: + Location = Point.Add( + new Point(screenBounds.Left, screenBounds.Bottom), + new Size(50, -(Height + 50))); + break; + case 2: + Location = Point.Add( + new Point(screenBounds.Right, screenBounds.Top), + new Size(-(Width + 50), 50)); + break; + case 3: + Location = Point.Add( + new Point(screenBounds.Left, screenBounds.Top), + new Size(50, 00)); + break; + } + _originalLocation = new Point(Location.X, Location.Y); + } + } + + private void DrawBorder(object? sender, PaintEventArgs e) + { + using (Pen pen = new Pen(_color)) + { + pen.Width = 6; + e.Graphics.DrawLine(pen, 0, 0, Width, 0); + e.Graphics.DrawLine(pen, 0, 0, 0, Height); + e.Graphics.DrawLine(pen, 0, Height, Width, Height); + e.Graphics.DrawLine(pen, Width, 0, Width, Height); + } + } + + protected override void WndProc(ref Message m) + { + + switch (m.Msg) + { + case DwmHelper.WM_DWMCOMPOSITIONCHANGED: + if (System.Environment.OSVersion.Version.Major >= 6 && DwmHelper.IsCompositionEnabled()) + { + var policy = DwmHelper.DWMNCRENDERINGPOLICY.Enabled; + DwmHelper.WindowSetAttribute(Handle, DwmHelper.DWMWINDOWATTRIBUTE.NCRenderingPolicy, (int)policy); + DwmHelper.WindowBorderlessDropShadow(Handle, 2); + m.Result = IntPtr.Zero; + } + break; + case 0x0084: + m.Result = (IntPtr)(-1); + return; + default: + break; + } + base.WndProc(ref m); + } + + private void DrawText(object? sender, PaintEventArgs e) + { + if (sender != null) + { + var label = (Label)sender; + e.Graphics.Clear(Color.Transparent); + using (var sf = new StringFormat()) + using (var brush = new SolidBrush(label.ForeColor)) + { + sf.Alignment = sf.LineAlignment = StringAlignment.Near; + e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit; + e.Graphics.DrawString(label.Text, label.Font, brush, label.ClientRectangle, sf); + } + } + } + + public Guid Guid { get => _guid; } + + private void AdjustText() + { + + } + + private void CloseNotification(object? sender, System.Timers.ElapsedEventArgs e) + { + try + { + Close(); + } + catch + { + try + { + this.Invoke(() => Close()); + } + catch + { + throw new Exception("blah"); + } + } + + _timer.Stop(); + _timer.Dispose(); + } } } diff --git a/ObservatoryCore/UI/NotificationForm.resx b/ObservatoryCore/UI/NotificationForm.resx index 1af7de1..f298a7b 100644 --- a/ObservatoryCore/UI/NotificationForm.resx +++ b/ObservatoryCore/UI/NotificationForm.resx @@ -1,64 +1,4 @@ - - - + diff --git a/ObservatoryCore/UI/PluginHelper.cs b/ObservatoryCore/UI/PluginHelper.cs index ceee265..cb0fc56 100644 --- a/ObservatoryCore/UI/PluginHelper.cs +++ b/ObservatoryCore/UI/PluginHelper.cs @@ -1,11 +1,7 @@ using Observatory.Framework.Interfaces; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography.X509Certificates; -using System.Speech.Synthesis; -using System.Text; -using System.Threading.Tasks; +using System.Collections; +using Observatory.PluginManagement; +using Observatory.Utils; namespace Observatory.UI { @@ -47,12 +43,20 @@ namespace Observatory.UI if (plugin.PluginUI.PluginUIType == Framework.PluginUI.UIType.Basic) uiPanels.Add(newItem, CreateBasicUI(plugin)); + else if (plugin.PluginUI.PluginUIType == Framework.PluginUI.UIType.Panel) + uiPanels.Add(newItem, (Panel)plugin.PluginUI.UI); } private static Panel CreateBasicUI(IObservatoryPlugin plugin) { Panel panel = new(); - var columnSorter = new DefaultSorter(); + + IObservatoryComparer columnSorter; + if (plugin.ColumnSorter != null) + columnSorter = plugin.ColumnSorter; + else + columnSorter = new DefaultSorter(); + ListView listView = new() { View = View.Details, @@ -62,7 +66,8 @@ namespace Observatory.UI BackColor = Color.FromArgb(64, 64, 64), ForeColor = Color.LightGray, GridLines = true, - ListViewItemSorter = columnSorter + ListViewItemSorter = columnSorter, + Font = new Font(new FontFamily("Segoe UI"), 10, FontStyle.Regular) }; foreach (var property in plugin.PluginUI.DataGrid.First().GetType().GetProperties()) @@ -75,20 +80,20 @@ namespace Observatory.UI if (e.Column == columnSorter.SortColumn) { // Reverse the current sort direction for this column. - if (columnSorter.Order == SortOrder.Ascending) + if (columnSorter.Order == 1) { - columnSorter.Order = SortOrder.Descending; + columnSorter.Order = -1; } else { - columnSorter.Order = SortOrder.Ascending; + columnSorter.Order = 1; } } else { // Set the column number that is to be sorted; default to ascending. columnSorter.SortColumn = e.Column; - columnSorter.Order = SortOrder.Ascending; + columnSorter.Order = 1; } listView.Sort(); }; @@ -97,20 +102,58 @@ namespace Observatory.UI plugin.PluginUI.DataGrid.CollectionChanged += (sender, e) => { - if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add && - e.NewItems != null) + listView.Invoke(() => { - foreach (var newItem in e.NewItems) + if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add && + e.NewItems != null) { - ListViewItem newListItem = new(); - foreach (var property in newItem.GetType().GetProperties()) + foreach (var newItem in e.NewItems) { - newListItem.SubItems.Add(property.GetValue(newItem)?.ToString()); + ListViewItem newListItem = new(); + foreach (var property in newItem.GetType().GetProperties()) + { + newListItem.SubItems.Add(property.GetValue(newItem)?.ToString()); + } + newListItem.SubItems.RemoveAt(0); + listView.Items.Add(newListItem); } - newListItem.SubItems.RemoveAt(0); - listView.Items.Add(newListItem); } - } + + if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove && + e.OldItems != null) + { + foreach (var oldItem in e.OldItems) + { + ListViewItem oldListItem = new(); + foreach (var property in oldItem.GetType().GetProperties()) + { + oldListItem.SubItems.Add(property.GetValue(oldItem)?.ToString()); + } + oldListItem.SubItems.RemoveAt(0); + + var itemToRemove = listView.Items.Cast().Where(i => i.SubItems.Cast().SequenceEqual(oldListItem.SubItems.Cast())).First(); + if (itemToRemove != null) + { + listView.Items.Remove(itemToRemove); + } + } + } + + if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Reset) + { + listView.Items.Clear(); + foreach (var item in plugin.PluginUI.DataGrid) + { + ListViewItem listItem = new(); + foreach (var property in item.GetType().GetProperties()) + { + listItem.SubItems.Add(property.GetValue(item)?.ToString()); + } + listItem.SubItems.RemoveAt(0); + listView.Items.Add(listItem); + } + } + }); }; return panel; diff --git a/ObservatoryCore/UI/SettingsPanel.cs b/ObservatoryCore/UI/SettingsPanel.cs index 36bc925..3c9da92 100644 --- a/ObservatoryCore/UI/SettingsPanel.cs +++ b/ObservatoryCore/UI/SettingsPanel.cs @@ -2,10 +2,12 @@ using Observatory.Framework.Interfaces; using System; using System.Collections.Generic; +using System.Data.Common; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; +using System.Windows.Forms.VisualStyles; namespace Observatory.UI { @@ -22,7 +24,7 @@ namespace Observatory.UI _adjustPanelsBelow = adjustPanelsBelow; // Filtered to only settings without SettingIgnore attribute - var settings = PluginManagement.PluginManager.GetSettingDisplayNames(plugin).Where(s => !Attribute.IsDefined(s.Key, typeof (SettingIgnore))); + var settings = PluginManagement.PluginManager.GetSettingDisplayNames(plugin.Settings).Where(s => !Attribute.IsDefined(s.Key, typeof (SettingIgnore))); CreateControls(settings); } @@ -30,35 +32,300 @@ namespace Observatory.UI private void CreateControls(IEnumerable> settings) { int controlRow = 0; - bool nextColumn = true; + bool recentHalfCol = false; - // Handle bool (checkbox) settings first and keep them grouped together - foreach (var setting in settings.Where(s => s.Key.PropertyType == typeof(bool))) + foreach (var setting in settings) { - CheckBox checkBox = new() + // Reset the column tracking for checkboxes if this isn't a checkbox + if (setting.Key.PropertyType.Name != "Boolean" && setting.Key.PropertyType.Name != "Button") + recentHalfCol = false; + + switch (setting.Key.GetValue(_plugin.Settings)) { - Text = setting.Value, - Checked = (bool?)setting.Key.GetValue(_plugin.Settings) ?? false - }; + case bool: + var checkBox = CreateBoolSetting(setting); + controlRow += recentHalfCol ? 0 : 1; + checkBox.Location = GetSettingPosition(controlRow, recentHalfCol); - checkBox.CheckedChanged += (object? _, EventArgs _) => - { - setting.Key.SetValue(_plugin.Settings, checkBox.Checked); - PluginManagement.PluginManager.GetInstance.SaveSettings(_plugin, _plugin.Settings); - }; + recentHalfCol = !recentHalfCol; - checkBox.Location = new Point(nextColumn ? 10 : 130, 3 + controlRow * 29); - controlRow += nextColumn ? 0 : 1; - nextColumn = !nextColumn; + Controls.Add(checkBox); + break; + case string: + var stringLabel = CreateSettingLabel(setting.Value); + var textBox = CreateStringSetting(setting.Key); + controlRow++; + stringLabel.Location = GetSettingPosition(controlRow); + textBox.Location = GetSettingPosition(controlRow, true); + + Controls.Add(stringLabel); + Controls.Add(textBox); - Controls.Add(checkBox); + break; + case FileInfo: + var fileLabel = CreateSettingLabel(setting.Value); + var pathTextBox = CreateFilePathSetting(setting.Key); + var pathButton = CreateFileBrowseSetting(setting.Key, pathTextBox); + + controlRow++; + + fileLabel.Location = GetSettingPosition(controlRow); + pathTextBox.Location = GetSettingPosition(controlRow, true); + pathButton.Location = GetSettingPosition(++controlRow, true); + + Controls.Add(fileLabel); + Controls.Add(pathTextBox); + Controls.Add(pathButton); + + break; + case int: + // We have two options for integer values: + // 1) A slider (explicit by way of the SettingIntegerUseSlider attribute and bounded to 0..100 by default) + // 2) A numeric up/down (default otherwise, and is unbounded by default). + // Bounds for both can be set via the SettingNumericBounds attribute, only the up/down uses Increment. + var intLabel = CreateSettingLabel(setting.Value); + Control intControl; + controlRow++; + if (System.Attribute.IsDefined(setting.Key, typeof(SettingNumericUseSlider))) + { + intControl = CreateSettingTrackbar(setting.Key); + } + else + { + intControl = CreateSettingNumericUpDown(setting.Key); + } + intLabel.Location = GetSettingPosition(controlRow); + intControl.Location = GetSettingPosition(controlRow, true); + + Controls.Add(intLabel); + Controls.Add(intControl); + break; + case Action action: + var button = CreateSettingButton(setting.Value, action); + + controlRow += recentHalfCol ? 0 : 1; + button.Location = GetSettingPosition(controlRow, recentHalfCol); + recentHalfCol = !recentHalfCol; + + Controls.Add(button); + break; + case Dictionary dictSetting: + var dictLabel = CreateSettingLabel(setting.Value); + var dropdown = CreateSettingDropdown(setting.Key, dictSetting); + controlRow++; + + dictLabel.Location = GetSettingPosition(controlRow); + dropdown.Location = GetSettingPosition(controlRow, true); + Controls.Add(dictLabel); + Controls.Add(dropdown); + break; + default: + break; + } + } + Height = 3 + controlRow * 29; + } + + private static Point GetSettingPosition(int rowNum, bool secondCol = false) + { + return new Point(10 + (secondCol ? 200 : 0), -26 + rowNum * 29); + } + + + private Label CreateSettingLabel(string settingName) + { + Label label = new() + { + Text = settingName + ": ", + TextAlign = System.Drawing.ContentAlignment.MiddleRight, + Width = 200, + ForeColor = Color.LightGray + }; + + return label; + } + + private ComboBox CreateSettingDropdown(PropertyInfo setting, Dictionary dropdownItems) + { + var backingValueName = (SettingBackingValue?)Attribute.GetCustomAttribute(setting, typeof(SettingBackingValue)); + + var backingValue = from s in PluginManagement.PluginManager.GetSettingDisplayNames(_plugin.Settings) + where s.Value == backingValueName?.BackingProperty + select s.Key; + + if (backingValue.Count() != 1) + throw new($"{_plugin.ShortName}: Dictionary settings must have exactly one backing value."); + + ComboBox comboBox = new() + { + Width = 200, + DropDownStyle = ComboBoxStyle.DropDownList + }; + + comboBox.Items.AddRange(dropdownItems.OrderBy(s => s.Key).Select(s => s.Key).ToArray()); + + string? currentSelection = backingValue.First().GetValue(_plugin.Settings)?.ToString(); + + if (currentSelection?.Length > 0) + { + comboBox.SelectedItem = currentSelection; } - // Then the rest - foreach (var setting in settings.Where(s => s.Key.PropertyType != typeof(bool))) + comboBox.SelectedValueChanged += (sender, e) => + { + backingValue.First().SetValue(_plugin.Settings, comboBox.SelectedItem.ToString()); + SaveSettings(); + }; + + return comboBox; + } + + private Button CreateSettingButton(string settingName, Action action) + { + Button button = new() + { + Text = settingName + }; + + button.Click += (sender, e) => + { + action.Invoke(); + SaveSettings(); + }; + + return button; + } + + private TrackBar CreateSettingTrackbar(PropertyInfo setting) + { + SettingNumericBounds? bounds = (SettingNumericBounds?)System.Attribute.GetCustomAttribute(setting, typeof(SettingNumericBounds)); + TrackBar trackBar = new () + { + Orientation = Orientation.Horizontal, + TickStyle = TickStyle.Both, + Width = 200, + Minimum = Convert.ToInt32(bounds?.Minimum ?? 0), + Maximum = Convert.ToInt32(bounds?.Maximum ?? 100) + }; + + trackBar.Value = (int?)setting.GetValue(_plugin.Settings) ?? 0; + + trackBar.ValueChanged += (sender, e) => + { + setting.SetValue(_plugin.Settings, trackBar.Value); + SaveSettings(); + }; + + return trackBar; + } + + private NumericUpDown CreateSettingNumericUpDown(PropertyInfo setting) + { + SettingNumericBounds? bounds = (SettingNumericBounds?)System.Attribute.GetCustomAttribute(setting, typeof(SettingNumericBounds)); + NumericUpDown numericUpDown = new() { - } + Width = 200, + Minimum = Convert.ToInt32(bounds?.Minimum ?? Int32.MinValue), + Maximum = Convert.ToInt32(bounds?.Maximum ?? Int32.MaxValue), + Increment = Convert.ToInt32(bounds?.Increment ?? 1) + }; + + numericUpDown.Value = (int?)setting.GetValue(_plugin.Settings) ?? 0; + + numericUpDown.ValueChanged += (sender, e) => + { + setting.SetValue(_plugin.Settings, numericUpDown.Value); + SaveSettings(); + }; + + return numericUpDown; + } + + private CheckBox CreateBoolSetting(KeyValuePair setting) + { + CheckBox checkBox = new() + { + Text = setting.Value, + TextAlign= System.Drawing.ContentAlignment.MiddleLeft, + Checked = (bool?)setting.Key.GetValue(_plugin.Settings) ?? false, + Width = 200, + ForeColor = Color.LightGray + }; + + checkBox.CheckedChanged += (sender, e) => + { + setting.Key.SetValue(_plugin.Settings, checkBox.Checked); + SaveSettings(); + }; + + return checkBox; + } + + private TextBox CreateStringSetting(PropertyInfo setting) + { + TextBox textBox = new() + { + Text = (setting.GetValue(_plugin.Settings) ?? String.Empty).ToString(), + Width = 200 + }; + + textBox.TextChanged += (object? sender, EventArgs e) => + { + setting.SetValue(_plugin.Settings, textBox.Text); + SaveSettings(); + }; + + return textBox; + } + + private TextBox CreateFilePathSetting(PropertyInfo setting) + { + var fileInfo = (FileInfo?)setting.GetValue(_plugin.Settings); + + TextBox textBox = new() + { + Text = fileInfo?.FullName ?? string.Empty, + Width = 200 + }; + + textBox.TextChanged += (object? sender, EventArgs e) => + { + setting.SetValue(_plugin.Settings, new FileInfo(textBox.Text)); + SaveSettings(); + }; + + return textBox; + } + + private Button CreateFileBrowseSetting(PropertyInfo setting, TextBox textBox) + { + Button button = new() + { + Text = "Browse" + }; + + button.Click += (object? sender, EventArgs e) => + { + var currentDir = ((FileInfo?)setting.GetValue(_plugin.Settings))?.DirectoryName; + + OpenFileDialog ofd = new OpenFileDialog() + { + Title = "Select File...", + Filter = "Lua files (*.lua)|*.lua|All files (*.*)|*.*", + FilterIndex = 0, + InitialDirectory = currentDir ?? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + }; + + var browseResult = ofd.ShowDialog(); + + if (browseResult == DialogResult.OK) + { + textBox.Text = ofd.FileName; + } + }; + + return button; } private Label CreateHeader(string pluginName) @@ -92,5 +359,10 @@ namespace Observatory.UI } this.Parent?.ResumeLayout(); } + + private void SaveSettings() + { + PluginManagement.PluginManager.GetInstance.SaveSettings(_plugin, _plugin.Settings); + } } } diff --git a/ObservatoryCore/Utils/LogMonitor.cs b/ObservatoryCore/Utils/LogMonitor.cs index 2fe2383..45fb68f 100644 --- a/ObservatoryCore/Utils/LogMonitor.cs +++ b/ObservatoryCore/Utils/LogMonitor.cs @@ -284,7 +284,7 @@ namespace Observatory.Utils if (Properties.Core.Default.JournalFolder != path) { Properties.Core.Default.JournalFolder = path; - Properties.Core.Default.Save(); + SettingsManager.Save(); } return logDirectory; diff --git a/ObservatoryCore/Utils/SettingsManager.cs b/ObservatoryCore/Utils/SettingsManager.cs new file mode 100644 index 0000000..c1d8410 --- /dev/null +++ b/ObservatoryCore/Utils/SettingsManager.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Observatory.Utils +{ + internal static class SettingsManager + { + internal static void Save() + { +#if DEBUG || RELEASE + Properties.Core.Default.Save(); +#elif PORTABLE + + Dictionary settings = new(); + + foreach (PropertyInfo property in Properties.Core.Default.GetType().GetProperties()) + { + if (property.CanRead && property.CanWrite && !property.GetIndexParameters().Any()) + settings.Add( + property.Name, + property.GetValue(Properties.Core.Default) + ); + } + + string serializedSettings = JsonSerializer.Serialize(settings, new JsonSerializerOptions() + { + ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve, + + }); + File.WriteAllText("Observatory.config", serializedSettings); +#endif + } + + internal static void Load() + { +#if PORTABLE + if (File.Exists("Observatory.config")) + { + string savedSettings = File.ReadAllText("Observatory.config"); + Dictionary? settings = JsonSerializer.Deserialize>(savedSettings); + if (settings != null) + { + var properties = Properties.Core.Default.GetType().GetProperties(); + + foreach (var savedProperty in settings) + { + + var currentProperty = properties.Where(p => p.Name == savedProperty.Key); + if (currentProperty.Any()) + { + JsonElement? value = (JsonElement?)savedProperty.Value; + var deserializedValue = value?.Deserialize(currentProperty.First().PropertyType); + currentProperty.First().SetValue(Properties.Core.Default, deserializedValue); + } + + } + } + } +#endif + } + } +} diff --git a/ObservatoryDev/ObservatoryDev.sln b/ObservatoryDev/ObservatoryDev.sln index a9a446e..151a08b 100644 --- a/ObservatoryDev/ObservatoryDev.sln +++ b/ObservatoryDev/ObservatoryDev.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30128.74 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32922.545 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObservatoryBotanist", "..\ObservatoryBotanist\ObservatoryBotanist.csproj", "{498F7360-D443-4D64-895C-9EAB5570D019}" EndProject @@ -16,27 +16,38 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Portable|Any CPU = Portable|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {498F7360-D443-4D64-895C-9EAB5570D019}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {498F7360-D443-4D64-895C-9EAB5570D019}.Debug|Any CPU.Build.0 = Debug|Any CPU + {498F7360-D443-4D64-895C-9EAB5570D019}.Portable|Any CPU.ActiveCfg = Portable|Any CPU + {498F7360-D443-4D64-895C-9EAB5570D019}.Portable|Any CPU.Build.0 = Portable|Any CPU {498F7360-D443-4D64-895C-9EAB5570D019}.Release|Any CPU.ActiveCfg = Release|Any CPU {498F7360-D443-4D64-895C-9EAB5570D019}.Release|Any CPU.Build.0 = Release|Any CPU {0E1C4F16-858E-4E53-948A-77D81A8F3395}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0E1C4F16-858E-4E53-948A-77D81A8F3395}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E1C4F16-858E-4E53-948A-77D81A8F3395}.Portable|Any CPU.ActiveCfg = Portable|Any CPU + {0E1C4F16-858E-4E53-948A-77D81A8F3395}.Portable|Any CPU.Build.0 = Portable|Any CPU {0E1C4F16-858E-4E53-948A-77D81A8F3395}.Release|Any CPU.ActiveCfg = Release|Any CPU {0E1C4F16-858E-4E53-948A-77D81A8F3395}.Release|Any CPU.Build.0 = Release|Any CPU {E0FCF2A2-BF56-4F4D-836B-92A0E8269192}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E0FCF2A2-BF56-4F4D-836B-92A0E8269192}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0FCF2A2-BF56-4F4D-836B-92A0E8269192}.Portable|Any CPU.ActiveCfg = Portable|Any CPU + {E0FCF2A2-BF56-4F4D-836B-92A0E8269192}.Portable|Any CPU.Build.0 = Portable|Any CPU {E0FCF2A2-BF56-4F4D-836B-92A0E8269192}.Release|Any CPU.ActiveCfg = Release|Any CPU {E0FCF2A2-BF56-4F4D-836B-92A0E8269192}.Release|Any CPU.Build.0 = Release|Any CPU {27ABA3B7-AB3C-465F-BA40-4F06BD803811}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {27ABA3B7-AB3C-465F-BA40-4F06BD803811}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27ABA3B7-AB3C-465F-BA40-4F06BD803811}.Portable|Any CPU.ActiveCfg = Portable|Any CPU + {27ABA3B7-AB3C-465F-BA40-4F06BD803811}.Portable|Any CPU.Build.0 = Portable|Any CPU {27ABA3B7-AB3C-465F-BA40-4F06BD803811}.Release|Any CPU.ActiveCfg = Release|Any CPU {27ABA3B7-AB3C-465F-BA40-4F06BD803811}.Release|Any CPU.Build.0 = Release|Any CPU {BC57225F-D89B-4853-A816-9AB4865E7AC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BC57225F-D89B-4853-A816-9AB4865E7AC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC57225F-D89B-4853-A816-9AB4865E7AC5}.Portable|Any CPU.ActiveCfg = Portable|Any CPU + {BC57225F-D89B-4853-A816-9AB4865E7AC5}.Portable|Any CPU.Build.0 = Portable|Any CPU {BC57225F-D89B-4853-A816-9AB4865E7AC5}.Release|Any CPU.ActiveCfg = Release|Any CPU {BC57225F-D89B-4853-A816-9AB4865E7AC5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection diff --git a/ObservatoryExplorer/DefaultCriteria.cs b/ObservatoryExplorer/DefaultCriteria.cs index 3afe5f2..1d23ca5 100644 --- a/ObservatoryExplorer/DefaultCriteria.cs +++ b/ObservatoryExplorer/DefaultCriteria.cs @@ -16,6 +16,10 @@ namespace Observatory.Explorer bool isRing = scan.BodyName.Contains("Ring"); +#if DEBUG + // results.Add("Test Scan Event", "Test Detail"); +#endif + #region Landable Checks if (scan.Landable) { diff --git a/ObservatoryExplorer/ObservatoryExplorer.csproj b/ObservatoryExplorer/ObservatoryExplorer.csproj index 4b0b304..0dac63d 100644 --- a/ObservatoryExplorer/ObservatoryExplorer.csproj +++ b/ObservatoryExplorer/ObservatoryExplorer.csproj @@ -5,6 +5,7 @@ enable false ObservatoryKey.snk + Debug;Release;Portable @@ -20,10 +21,10 @@ - - - - + + + + diff --git a/ObservatoryFramework/Interfaces.cs b/ObservatoryFramework/Interfaces.cs index 3f7e0e3..27ee27b 100644 --- a/ObservatoryFramework/Interfaces.cs +++ b/ObservatoryFramework/Interfaces.cs @@ -1,7 +1,6 @@ -using System; -using System.Net.Http; -using Observatory.Framework.Files; +using Observatory.Framework.Files; using Observatory.Framework.Files.Journal; +using System.Xml.XPath; namespace Observatory.Framework.Interfaces { @@ -50,6 +49,13 @@ namespace Observatory.Framework.Interfaces /// public object Settings { get; set; } + /// + /// Plugin-specific object implementing the IComparer interface which is used to sort columns in the basic UI datagrid. + /// If omitted a basic numeric compare sorter is used. + /// + public IObservatoryComparer ColumnSorter + { get => null; } + } /// @@ -163,13 +169,6 @@ namespace Observatory.Framework.Interfaces /// Grid items to be added. Object types should match original template item used to create the grid. public void AddGridItems(IObservatoryWorker worker, IEnumerable items); - /// - /// Add multiple items to the bottom of the basic UI grid. - /// - /// Reference to the calling plugin's worker interface. - /// Grid items to be added. Object types should match original template item used to create the grid. - public void AddGridItems(IObservatoryWorker worker, IEnumerable items); - /// /// Clears basic UI grid, removing all items. /// @@ -221,4 +220,21 @@ namespace Observatory.Framework.Interfaces /// public string PluginStorageFolder { get; } } + + /// + /// Extends the base IComparer interface with exposed values for the column ID and sort order to use. + /// + public interface IObservatoryComparer : System.Collections.IComparer + { + /// + /// Column ID to be currently sorted by. + /// + public int SortColumn { get; set; } + + /// + /// Current order of sorting. Ascending = 1, Descending = -1, No sorting = 0. + /// + public int Order { get; set; } + } + } diff --git a/ObservatoryFramework/ObservatoryFramework.csproj b/ObservatoryFramework/ObservatoryFramework.csproj index 8da2df4..c07018a 100644 --- a/ObservatoryFramework/ObservatoryFramework.csproj +++ b/ObservatoryFramework/ObservatoryFramework.csproj @@ -4,6 +4,7 @@ net6.0 enable Observatory.Framework + Debug;Release;Portable @@ -17,5 +18,9 @@ ObservatoryFramework.xml + + + ObservatoryFramework.xml + diff --git a/ObservatoryFramework/ObservatoryFramework.xml b/ObservatoryFramework/ObservatoryFramework.xml index a046077..2b3a07a 100644 --- a/ObservatoryFramework/ObservatoryFramework.xml +++ b/ObservatoryFramework/ObservatoryFramework.xml @@ -1350,6 +1350,12 @@ If a public property is necessary but not required to be user accessible the [SettingIgnore] property will suppress display. + + + Plugin-specific object implementing the IComparer interface which is used to sort columns in the basic UI datagrid. + If omitted a basic string compare sorter is used. + + Interface for worker plugins which process journal data to update their UI or send notifications. @@ -1504,6 +1510,21 @@ Retrieves and ensures creation of a location which can be used by the plugin to store persistent data. + + + Extends the base IComparer interface with exposed values for the column ID and sort order to use. + + + + + Column ID to be currently sorted by. + + + + + Current order of sorting. Ascending = 1, Descending = -1, No sorting = 0. + + Class permitting plugins to provide their UI, if any, to Observatory Core. @@ -1516,13 +1537,12 @@ - UI object used by plugins with UIType.Avalonia. - (Untested/not implemented) + UI object used by plugins with UIType.Panel. - Collection bound to DataGrid used byu plugins with UIType.Basic. + Collection bound to DataGrid used by plugins with UIType.Basic. Objects in collection should be of a class defined within the plugin consisting of string properties.
Each object is a single row, and the property names are used as column headers.
@@ -1531,7 +1551,7 @@ Instantiate PluginUI of UIType.Basic.
- Collection bound to DataGrid used byu plugins with UIType.Basic. + Collection bound to DataGrid used by plugins with UIType.Basic. Objects in collection should be of a class defined within the plugin consisting of string properties.
Each object is a single row, and the property names are used as column headers.
diff --git a/ObservatoryFramework/PluginUI.cs b/ObservatoryFramework/PluginUI.cs index 0e73312..c65edbc 100644 --- a/ObservatoryFramework/PluginUI.cs +++ b/ObservatoryFramework/PluginUI.cs @@ -1,6 +1,7 @@ using System; using System.Collections.ObjectModel; using System.Linq; +using System.Security.Principal; using System.Text; using System.Threading.Tasks; @@ -17,13 +18,12 @@ namespace Observatory.Framework public readonly UIType PluginUIType; /// - /// UI object used by plugins with UIType.Avalonia. - /// (Untested/not implemented) + /// UI object used by plugins with UIType.Panel. /// public object UI; /// - /// Collection bound to DataGrid used byu plugins with UIType.Basic. + /// Collection bound to DataGrid used by plugins with UIType.Basic. /// Objects in collection should be of a class defined within the plugin consisting of string properties.
Each object is a single row, and the property names are used as column headers.
///
public ObservableCollection DataGrid; @@ -32,7 +32,7 @@ namespace Observatory.Framework /// Instantiate PluginUI of UIType.Basic. /// /// - /// Collection bound to DataGrid used byu plugins with UIType.Basic. + /// Collection bound to DataGrid used by plugins with UIType.Basic. /// Objects in collection should be of a class defined within the plugin consisting of string properties.
Each object is a single row, and the property names are used as column headers.
/// public PluginUI(ObservableCollection DataGrid) diff --git a/ObservatoryHerald/ObservatoryHerald.csproj b/ObservatoryHerald/ObservatoryHerald.csproj index 18db54b..312ef56 100644 --- a/ObservatoryHerald/ObservatoryHerald.csproj +++ b/ObservatoryHerald/ObservatoryHerald.csproj @@ -4,6 +4,7 @@ net6.0 enable true + Debug;Release;Portable