2
0
mirror of https://github.com/9ParsonsB/Pulsar.git synced 2025-07-01 08:23:42 -04:00

ready for testing

This commit is contained in:
Xjph
2024-01-21 13:35:03 -03:30
parent d99a190869
commit 97e981bae2
92 changed files with 3061 additions and 1186 deletions

View File

@ -70,6 +70,12 @@
<setting name="StartReadAll" serializeAs="String">
<value>False</value>
</setting>
<setting name="Theme" serializeAs="String">
<value>Dark</value>
</setting>
<setting name="ColumnSizing" serializeAs="String">
<value />
</setting>
</Observatory.Properties.Core>
</userSettings>
</configuration>

View File

@ -0,0 +1,73 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Observatory.Assets {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Observatory.Assets.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
/// </summary>
internal static System.Drawing.Icon EOCIcon_Presized {
get {
object obj = ResourceManager.GetObject("EOCIcon_Presized", resourceCulture);
return ((System.Drawing.Icon)(obj));
}
}
}
}

View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="EOCIcon_Presized" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>EOCIcon-Presized.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root>

View File

@ -1,5 +1,6 @@
using Observatory.Framework;
using Observatory.UI;
using System;
namespace Observatory.NativeNotification
{
@ -21,6 +22,11 @@ namespace Observatory.NativeNotification
notification.FormClosed += NotifyWindow_Closed;
foreach(var notificationForm in notifications)
{
notificationForm.Value.AdjustOffset(true);
}
notifications.Add(notificationGuid, notification);
notification.Show();
});
@ -34,6 +40,11 @@ namespace Observatory.NativeNotification
{
var currentNotification = (NotificationForm)sender;
foreach (var notification in notifications.Where(n => n.Value.CreationTime < currentNotification.CreationTime))
{
notification.Value.AdjustOffset(false);
}
if (notifications.ContainsKey(currentNotification.Guid))
{
notifications.Remove(currentNotification.Guid);

View File

@ -10,7 +10,7 @@ namespace Observatory.NativeNotification
{
public class NativeVoice
{
private Queue<NotificationArgs> notificationEvents;
private readonly Queue<NotificationArgs> notificationEvents;
private bool processing;
public NativeVoice()
@ -83,20 +83,24 @@ namespace Observatory.NativeNotification
processing = false;
}
private string AddVoiceToSsml(string ssml, string voiceName)
private static string AddVoiceToSsml(string ssml, string voiceName)
{
XmlDocument ssmlDoc = new();
ssmlDoc.LoadXml(ssml);
var ssmlNamespace = ssmlDoc.DocumentElement.NamespaceURI;
var ssmlNamespace = ssmlDoc.DocumentElement?.NamespaceURI;
XmlNamespaceManager ssmlNs = new(ssmlDoc.NameTable);
ssmlNs.AddNamespace("ssml", ssmlNamespace);
ssmlNs.AddNamespace("ssml", ssmlNamespace ?? string.Empty);
var voiceNode = ssmlDoc.SelectSingleNode("/ssml:speak/ssml:voice", ssmlNs);
voiceNode.Attributes.GetNamedItem("name").Value = voiceName;
var voiceNameNode = voiceNode?.Attributes?.GetNamedItem("name");
if (voiceNameNode != null)
{
voiceNameNode.Value = voiceName;
}
return ssmlDoc.OuterXml;
}
}

View File

@ -22,8 +22,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Security.Extensions" Version="1.2.0" />
<PackageReference Include="System.Speech" Version="7.0.0" />
<PackageReference Include="Microsoft.Security.Extensions" Version="1.3.0" />
<PackageReference Include="NAudio" Version="2.2.1" />
<PackageReference Include="System.Speech" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
@ -33,6 +34,11 @@
</ItemGroup>
<ItemGroup>
<Compile Update="Assets\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Properties\Core.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
@ -46,6 +52,10 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Assets\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
@ -58,6 +68,10 @@
<LastGenOutput>Core.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Condition=" '$(OS)' == 'Windows_NT'" Command="if not exist &quot;$(ProjectDir)..\ObservatoryFramework\bin\Release\net6.0\ObservatoryFramework.dll&quot; dotnet build &quot;$(ProjectDir)..\ObservatoryFramework\ObservatoryFramework.csproj&quot; -c Release" />

View File

@ -14,11 +14,15 @@ namespace Observatory.PluginManagement
private readonly NativeVoice NativeVoice;
private readonly NativePopup NativePopup;
public PluginCore()
private bool OverridePopup;
private bool OverrideAudio;
public PluginCore(bool OverridePopup = false, bool OverrideAudio = false)
{
NativeVoice = new();
NativePopup = new();
this.OverridePopup = OverridePopup;
this.OverrideAudio = OverrideAudio;
}
public string Version => System.Reflection.Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0";
@ -31,11 +35,8 @@ namespace Observatory.PluginManagement
};
}
public Status GetStatus()
{
throw new NotImplementedException();
}
public Status GetStatus() => LogMonitor.GetInstance.Status;
public Guid SendNotification(string title, string text)
{
return SendNotification(new NotificationArgs() { Title = title, Detail = text });
@ -53,12 +54,12 @@ namespace Observatory.PluginManagement
handler?.Invoke(this, notificationArgs);
}
if (Properties.Core.Default.NativeNotify && notificationArgs.Rendering.HasFlag(NotificationRendering.NativeVisual))
if (!OverridePopup && Properties.Core.Default.NativeNotify && notificationArgs.Rendering.HasFlag(NotificationRendering.NativeVisual))
{
guid = NativePopup.InvokeNativeNotification(notificationArgs);
}
if (Properties.Core.Default.VoiceNotify && notificationArgs.Rendering.HasFlag(NotificationRendering.NativeVocal))
if (!OverrideAudio && Properties.Core.Default.VoiceNotify && notificationArgs.Rendering.HasFlag(NotificationRendering.NativeVocal))
{
NativeVoice.EnqueueAndAnnounce(notificationArgs);
}
@ -69,14 +70,13 @@ namespace Observatory.PluginManagement
public void CancelNotification(Guid id)
{
NativePopup.CloseNotification(id);
ExecuteOnUIThread(() => NativePopup.CloseNotification(id));
}
public void UpdateNotification(Guid id, NotificationArgs notificationArgs)
{
if (!IsLogMonitorBatchReading)
{
if (notificationArgs.Rendering.HasFlag(NotificationRendering.PluginNotifier))
{
var handler = Notification;
@ -140,6 +140,8 @@ namespace Observatory.PluginManagement
public event EventHandler<NotificationArgs> Notification;
internal event EventHandler<PluginMessageArgs> PluginMessage;
public string PluginStorageFolder
{
get
@ -161,25 +163,19 @@ namespace Observatory.PluginManagement
}
}
public async Task PlayAudioFile(string filePath)
{
await AudioHandler.PlayFile(filePath);
}
public void SendPluginMessage(IObservatoryPlugin plugin, object message)
{
PluginMessage?.Invoke(this, new PluginMessageArgs(plugin.Name, plugin.Version, message));
}
internal void Shutdown()
{
NativePopup.CloseAll();
}
private static bool FirstRowIsAllNull(IObservatoryWorker worker)
{
bool allNull = true;
Type itemType = worker.PluginUI.DataGrid[0].GetType();
foreach (var property in itemType.GetProperties())
{
if (property.GetValue(worker.PluginUI.DataGrid[0], null) != null)
{
allNull = false;
break;
}
}
return allNull;
}
}
}

View File

@ -110,6 +110,14 @@ namespace Observatory.PluginManagement
}
}
public void OnPluginMessageEvent(object _, PluginMessageArgs messageArgs)
{
foreach (var plugin in observatoryNotifiers.Cast<IObservatoryPlugin>().Concat(observatoryWorkers))
{
plugin.HandlePluginMessage(messageArgs.SourceName, messageArgs.SourceVersion, messageArgs.Message);
}
}
private void ResetTimer()
{
timer.Stop();
@ -146,4 +154,18 @@ namespace Observatory.PluginManagement
}
}
}
internal class PluginMessageArgs
{
internal string SourceName;
internal string SourceVersion;
internal object Message;
internal PluginMessageArgs(string sourceName, string sourceVersion, object message)
{
SourceName = sourceName;
SourceVersion = sourceVersion;
Message = message;
}
}
}

View File

@ -50,7 +50,10 @@ namespace Observatory.PluginManagement
logMonitor.StatusUpdate += pluginHandler.OnStatusUpdate;
logMonitor.LogMonitorStateChanged += pluginHandler.OnLogMonitorStateChanged;
core = new PluginCore();
var ovPopup = notifyPlugins.Any(n => n.plugin.OverridePopupNotifications);
var ovAudio = notifyPlugins.Any(n => n.plugin.OverrideAudioNotifications);
core = new PluginCore(ovPopup, ovAudio);
List<IObservatoryPlugin> errorPlugins = new();
@ -97,6 +100,7 @@ namespace Observatory.PluginManagement
notifyPlugins.RemoveAll(n => errorPlugins.Contains(n.plugin));
core.Notification += pluginHandler.OnNotificationEvent;
core.PluginMessage += pluginHandler.OnPluginMessageEvent;
if (errorList.Any())
ErrorReporter.ShowErrorPopup("Plugin Load Error" + (errorList.Count > 1 ? "s" : String.Empty), errorList);
@ -114,7 +118,15 @@ namespace Observatory.PluginManagement
if (!String.IsNullOrWhiteSpace(savedSettings))
{
pluginSettings = JsonSerializer.Deserialize<Dictionary<string, object>>(savedSettings);
var settings = JsonSerializer.Deserialize<Dictionary<string, object>>(savedSettings);
if (settings != null)
{
pluginSettings = settings;
}
else
{
pluginSettings = new();
}
}
else
{
@ -138,7 +150,7 @@ namespace Observatory.PluginManagement
var properties = settings.GetType().GetProperties();
foreach (var property in properties)
{
var attrib = property.GetCustomAttribute<Framework.SettingDisplayName>();
var attrib = property.GetCustomAttribute<SettingDisplayName>();
if (attrib == null)
{
settingNames.Add(property, property.Name);
@ -396,7 +408,7 @@ namespace Observatory.PluginManagement
if (constructor != null)
{
object instance = constructor.Invoke(Array.Empty<object>());
notifiers.Add(((instance as IObservatoryNotifier)!, PluginStatus.Signed));
notifiers.Add(((instance as IObservatoryNotifier)!, pluginStatus));
pluginCount++;
}
}

View File

@ -12,7 +12,7 @@ namespace Observatory.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.3.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.8.0.0")]
internal sealed partial class Core : global::System.Configuration.ApplicationSettingsBase {
private static Core defaultInstance = ((Core)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Core())));
@ -285,5 +285,29 @@ namespace Observatory.Properties {
this["UnsignedAllowed"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("Dark")]
public string Theme {
get {
return ((string)(this["Theme"]));
}
set {
this["Theme"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")]
public string ColumnSizing {
get {
return ((string)(this["ColumnSizing"]));
}
set {
this["ColumnSizing"] = value;
}
}
}
}

View File

@ -68,5 +68,11 @@
<Setting Name="UnsignedAllowed" Type="System.Collections.Specialized.StringCollection" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="Theme" Type="System.String" Scope="User">
<Value Profile="(Default)">Dark</Value>
</Setting>
<Setting Name="ColumnSizing" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
</Settings>
</SettingsFile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Observatory.UI
{
public class ColumnSizing
{
public string PluginName { get; set; }
public string PluginVersion { get; set; }
public Dictionary<string, int> ColumnWidth
{
get
{
_columnWidth ??= new Dictionary<string, int>();
return _columnWidth;
}
set => _columnWidth = value;
}
private Dictionary<string, int>? _columnWidth;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,34 @@
using Observatory.PluginManagement;
using Observatory.Framework.Interfaces;
using System.Linq;
namespace Observatory.UI
{
partial class CoreForm
{
private Dictionary<ListViewItem, IObservatoryPlugin>? ListedPlugins;
private void PopulatePluginList()
{
List<IObservatoryPlugin> uniquePlugins = new();
ListedPlugins = new();
foreach (var (plugin, signed) in PluginManager.GetInstance.workerPlugins)
{
if (!uniquePlugins.Contains(plugin))
if (!ListedPlugins.ContainsValue(plugin))
{
uniquePlugins.Add(plugin);
ListViewItem item = new ListViewItem(new[] { plugin.Name, "Worker", plugin.Version, PluginStatusString(signed) });
ListedPlugins.Add(item, plugin);
PluginList.Items.Add(item);
}
}
foreach (var (plugin, signed) in PluginManager.GetInstance.notifyPlugins)
{
if (!uniquePlugins.Contains(plugin))
if (!ListedPlugins.ContainsValue(plugin))
{
uniquePlugins.Add(plugin);
ListViewItem item = new ListViewItem(new[] { plugin.Name, "Notifier", plugin.Version, PluginStatusString(signed) });
ListedPlugins.Add(item, plugin);
PluginList.Items.Add(item);
}
}
@ -71,39 +74,87 @@ namespace Observatory.UI
{
pluginList.Add(item.Text, item);
}
CoreMenu.Width = GetExpandedMenuWidth();
}
private void CreatePluginSettings()
private void DisableOverriddenNotification()
{
foreach (var plugin in PluginManager.GetInstance.workerPlugins)
var notifyPlugins = PluginManager.GetInstance.notifyPlugins;
var ovPopupPlugins = notifyPlugins.Where(n => n.plugin.OverridePopupNotifications);
if (ovPopupPlugins.Any())
{
var pluginSettingsPanel = new SettingsPanel(plugin.plugin, AdjustPanelsBelow);
AddSettingsPanel(pluginSettingsPanel);
PopupCheckbox.Checked = false;
PopupCheckbox.Enabled = false;
DisplayDropdown.Enabled = false;
CornerDropdown.Enabled = false;
FontDropdown.Enabled = false;
ScaleSpinner.Enabled = false;
DurationSpinner.Enabled = false;
ColourButton.Enabled = false;
TestButton.Enabled = false;
var pluginNames = string.Join(", ", ovPopupPlugins.Select(o => o.plugin.ShortName));
PopupSettingsPanel.MouseMove += (_, _) =>
{
OverrideTooltip.SetToolTip(PopupSettingsPanel, "Disabled by plugin: " + pluginNames);
};
}
foreach (var plugin in PluginManager.GetInstance.notifyPlugins)
var ovAudioPlugins = notifyPlugins.Where(n => n.plugin.OverrideAudioNotifications);
if (ovAudioPlugins.Any())
{
var pluginSettingsPanel = new SettingsPanel(plugin.plugin, AdjustPanelsBelow);
AddSettingsPanel(pluginSettingsPanel);
VoiceCheckbox.Checked = false;
VoiceCheckbox.Enabled = false;
VoiceVolumeSlider.Enabled = false;
VoiceSpeedSlider.Enabled = false;
VoiceDropdown.Enabled = false;
VoiceTestButton.Enabled = false;
var pluginNames = string.Join(", ", ovAudioPlugins.Select(o => o.plugin.ShortName));
VoiceSettingsPanel.MouseMove += (_, _) =>
{
OverrideTooltip.SetToolTip(VoiceSettingsPanel, "Disabled by plugin: " + pluginNames);
};
}
}
private void AddSettingsPanel(SettingsPanel panel)
private int GetExpandedMenuWidth()
{
int lowestPoint = 0;
foreach (Control control in CorePanel.Controls)
int maxWidth = 0;
foreach (ToolStripMenuItem item in CoreMenu.Items)
{
if (control.Location.Y + control.Height > lowestPoint)
lowestPoint = control.Location.Y + control.Height;
var itemWidth = TextRenderer.MeasureText(item.Text, item.Font);
maxWidth = itemWidth.Width > maxWidth ? itemWidth.Width : maxWidth;
}
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);
return maxWidth + 5;
}
private void PluginSettingsButton_Click(object sender, EventArgs e)
{
if (ListedPlugins != null && PluginList.SelectedItems.Count != 0)
{
var plugin = ListedPlugins[PluginList.SelectedItems[0]];
if (SettingsForms.ContainsKey(plugin))
{
SettingsForms[plugin].Activate();
}
else
{
SettingsForm settingsForm = new(plugin);
SettingsForms.Add(plugin, settingsForm);
settingsForm.FormClosed += (_, _) => SettingsForms.Remove(plugin);
settingsForm.Show();
}
}
}
private Dictionary<IObservatoryPlugin, SettingsForm> SettingsForms = new();
}
}

View File

@ -2,6 +2,7 @@
using Observatory.Framework.Interfaces;
using Observatory.PluginManagement;
using Observatory.Utils;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
@ -9,10 +10,25 @@ namespace Observatory.UI
{
public partial class CoreForm : Form
{
private Dictionary<object, Panel> uiPanels;
private readonly Dictionary<object, Panel> uiPanels;
[DllImport("user32.dll")]
private static extern int SendMessage(IntPtr hWnd, Int32 wMsg, bool wParam, Int32 lParam);
private const int WM_SETREDRAW = 11;
private static void SuspendDrawing(Control control)
{
SendMessage(control.Handle, WM_SETREDRAW, false, 0);
}
private static void ResumeDrawing(Control control)
{
SendMessage(control.Handle, WM_SETREDRAW, true, 0);
control.Refresh();
}
public CoreForm()
{
DoubleBuffered = true;
InitializeComponent();
PopulateDropdownOptions();
@ -24,14 +40,20 @@ namespace Observatory.UI
string version = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0";
Text += $" - v{version}";
CoreMenu.SizeChanged += CoreMenu_SizeChanged;
uiPanels = new();
uiPanels.Add(coreToolStripMenuItem, CorePanel);
uiPanels = new()
{
{ coreToolStripMenuItem, CorePanel }
};
pluginList = new Dictionary<string, ToolStripMenuItem>();
CreatePluginTabs();
CreatePluginSettings();
DisableOverriddenNotification();
CoreMenu.ItemClicked += CoreMenu_ItemClicked;
PreCollapsePanels();
ThemeManager.GetInstance.RegisterControl(this);
}
private void PreCollapsePanels()
@ -47,17 +69,7 @@ namespace Observatory.UI
}
private Dictionary<string, ToolStripMenuItem> pluginList;
private static void DuplicateControlVisuals(Control source, Control target, bool applyHeight = true)
{
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 readonly Dictionary<string, ToolStripMenuItem> pluginList;
private void ToggleMonitorButton_Click(object sender, EventArgs e)
{
@ -73,9 +85,25 @@ namespace Observatory.UI
}
}
private void CoreMenu_ItemClicked(object? _, ToolStripItemClickedEventArgs e)
private void ResizePanels(Point location, int widthChange)
{
CorePanel.Location = location;
CorePanel.Width += widthChange;
foreach (var panel in uiPanels)
{
if (Controls.Contains(panel.Value))
{
panel.Value.Location = CorePanel.Location;
panel.Value.Size = CorePanel.Size;
}
}
}
private void CoreMenu_ItemClicked(object? _, ToolStripItemClickedEventArgs e)
{
SuspendDrawing(this);
if (e.ClickedItem.Text == "<")
{
foreach (KeyValuePair<string, ToolStripMenuItem> menuItem in pluginList)
@ -86,8 +114,7 @@ namespace Observatory.UI
menuItem.Value.Text = menuItem.Key[..1];
}
CoreMenu.Width = 40;
CorePanel.Location = new Point(43, 12);
// CorePanel.Width += 40;
ResizePanels(new Point(43, 12), 0);
}
else if (e.ClickedItem.Text == ">")
{
@ -98,9 +125,8 @@ namespace Observatory.UI
else
menuItem.Value.Text = menuItem.Key;
}
CoreMenu.Width = 120;
CorePanel.Location = new Point(123, 12);
// CorePanel.Width -= 40;
CoreMenu.Width = GetExpandedMenuWidth();
ResizePanels(new Point(CoreMenu.Width + 3, 12), 0);
}
else
{
@ -114,26 +140,38 @@ namespace Observatory.UI
uiPanels[e.ClickedItem].Location = CorePanel.Location;
uiPanels[e.ClickedItem].Size = CorePanel.Size;
uiPanels[e.ClickedItem].BackColor = CorePanel.BackColor;
uiPanels[e.ClickedItem].Parent = CorePanel.Parent;
Controls.Add(uiPanels[e.ClickedItem]);
}
uiPanels[e.ClickedItem].Visible = true;
SetClickedItem(e.ClickedItem);
}
ResumeDrawing(this);
}
private void SetClickedItem(ToolStripItem item)
{
foreach (ToolStripItem menuItem in CoreMenu.Items)
{
bool bold = menuItem == item;
menuItem.Font = new Font(menuItem.Font, bold ? FontStyle.Bold : FontStyle.Regular);
}
}
private static void ColourListHeader(ref ListView list, Color backColor, Color foreColor)
{
list.OwnerDraw = true;
list.DrawColumnHeader +=
new DrawListViewColumnHeaderEventHandler
(
(sender, e) => headerDraw(sender, e, backColor, foreColor)
(sender, e) => HeaderDraw(sender, e, backColor, foreColor)
);
list.DrawItem += new DrawListViewItemEventHandler(bodyDraw);
list.DrawItem += new DrawListViewItemEventHandler(BodyDraw);
}
private static void headerDraw(object? _, DrawListViewColumnHeaderEventArgs e, Color backColor, Color foreColor)
private static void HeaderDraw(object? _, DrawListViewColumnHeaderEventArgs e, Color backColor, Color foreColor)
{
using (SolidBrush backBrush = new(backColor))
{
@ -149,17 +187,19 @@ namespace Observatory.UI
if (e.Font != null && e.Header != null)
using (SolidBrush foreBrush = new(foreColor))
{
var format = new StringFormat();
format.Alignment = (StringAlignment)e.Header.TextAlign;
format.LineAlignment = StringAlignment.Center;
var format = new StringFormat
{
Alignment = (StringAlignment)e.Header.TextAlign,
LineAlignment = StringAlignment.Center
};
var paddedBounds = new Rectangle(e.Bounds.X + 2, e.Bounds.Y + 2, e.Bounds.Width - 4, e.Bounds.Height - 4);
e.Graphics.DrawString(e.Header?.Text, e.Font, foreBrush, paddedBounds, format);
}
}
private static void bodyDraw(object? _, DrawListViewItemEventArgs e)
private static void BodyDraw(object? _, DrawListViewItemEventArgs e)
{
e.DrawDefault = true;
}
@ -180,7 +220,11 @@ namespace Observatory.UI
private void ReadAllButton_Click(object sender, EventArgs e)
{
LogMonitor.GetInstance.ReadAllJournals();
var readAllDialogue = new ReadAllForm();
ThemeManager.GetInstance.RegisterControl(readAllDialogue);
readAllDialogue.StartPosition = FormStartPosition.Manual;
readAllDialogue.Location = Point.Add(Location, new Size(100,100));
readAllDialogue.ShowDialog();
}
private void PopupNotificationLabel_Click(object _, EventArgs e)
@ -238,6 +282,8 @@ namespace Observatory.UI
Up, Down
}
private Observatory.NativeNotification.NativePopup? nativePopup;
private void TestButton_Click(object sender, EventArgs e)
{
NotificationArgs args = new()
@ -245,9 +291,10 @@ namespace Observatory.UI
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();
nativePopup ??= new Observatory.NativeNotification.NativePopup();
nativePopup.InvokeNativeNotification(args);
}
}
}

View File

@ -1,4 +1,64 @@
<root>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
@ -63,6 +123,9 @@
<metadata name="PopupColour.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>126, 17</value>
</metadata>
<metadata name="OverrideTooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>251, 17</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>

View File

@ -28,55 +28,54 @@
/// </summary>
private void InitializeComponent()
{
this.Title = new System.Windows.Forms.Label();
this.Body = new System.Windows.Forms.Label();
this.SuspendLayout();
Title = new Label();
Body = new Label();
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";
Title.Font = new Font("Segoe UI", 24F, FontStyle.Regular, GraphicsUnit.Point);
Title.ForeColor = Color.OrangeRed;
Title.Location = new Point(5, 5);
Title.MaximumSize = new Size(355, 0);
Title.Name = "Title";
Title.Size = new Size(338, 45);
Title.TabIndex = 0;
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;
Body.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
Body.AutoSize = true;
Body.Font = new Font("Segoe UI", 14.25F, FontStyle.Regular, GraphicsUnit.Point);
Body.ForeColor = Color.OrangeRed;
Body.Location = new Point(12, 45);
Body.MaximumSize = new Size(320, 85);
Body.Name = "Body";
Body.Size = new Size(51, 31);
Body.TabIndex = 1;
Body.Text = "Body";
Body.UseCompatibleTextRendering = true;
//
// NotificationForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
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();
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
BackColor = Color.FromArgb(64, 64, 64);
ClientSize = new Size(355, 145);
ControlBox = false;
Controls.Add(Body);
Controls.Add(Title);
Enabled = false;
FormBorderStyle = FormBorderStyle.None;
MaximizeBox = false;
MinimizeBox = false;
Name = "NotificationForm";
ShowIcon = false;
ShowInTaskbar = false;
Text = "NotificationForm";
ResumeLayout(false);
PerformLayout();
}
#endregion

View File

@ -23,7 +23,7 @@ namespace Observatory.UI
protected override bool ShowWithoutActivation => true;
protected override CreateParams CreateParams
{
{
get
{
CreateParams cp = base.CreateParams;
@ -31,11 +31,12 @@ namespace Observatory.UI
return cp;
}
}
public NotificationForm(Guid guid, NotificationArgs args)
{
_guid = guid;
_color = Color.FromArgb((int)Properties.Core.Default.NativeNotifyColour);
CreationTime = DateTime.Now;
InitializeComponent();
Title.Paint += DrawText;
@ -65,8 +66,8 @@ namespace Observatory.UI
Body.ForeColor = _color;
Body.Text = args.Detail;
Body.Font = new Font(Properties.Core.Default.NativeNotifyFont, 14);
this.Paint += DrawBorder;
Paint += DrawBorder;
AdjustPosition(args.XPos / 100, args.YPos / 100);
_timer = new();
@ -78,10 +79,36 @@ namespace Observatory.UI
}
}
private void NotificationForm_FormClosed(object? sender, FormClosedEventArgs e)
{
throw new NotImplementedException();
}
public DateTime CreationTime { get; private init; }
public void Update(NotificationArgs notificationArgs)
{
Title.Text = notificationArgs.Title;
Body.Text = notificationArgs.Detail;
// Catch Cross-thread access and invoke
try
{
Title.Text = notificationArgs.Title;
Body.Text = notificationArgs.Detail;
}
catch
{
try
{
Invoke(() =>
{
Title.Text = notificationArgs.Title;
Body.Text = notificationArgs.Detail;
});
}
catch (Exception ex)
{
throw new Exception("Notification Update Failure, please inform Vithigar. Details: " + ex.Message);
}
}
}
private void AdjustPosition(double x = -1.0, double y = -1.0)
@ -90,7 +117,6 @@ namespace Observatory.UI
int corner = Properties.Core.Default.NativeNotifyCorner;
Rectangle screenBounds;
if (screen == -1 || screen > Screen.AllScreens.Length)
if (Screen.AllScreens.Length == 1)
screenBounds = Screen.GetBounds(this);
@ -115,7 +141,7 @@ namespace Observatory.UI
case 0:
Location = Point.Add(
new Point(screenBounds.Right, screenBounds.Bottom),
new Size(-(Width+50), -(Height+50)));
new Size(-(Width + 50), -(Height + 50)));
break;
case 1:
Location = Point.Add(
@ -151,7 +177,7 @@ namespace Observatory.UI
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case DwmHelper.WM_DWMCOMPOSITIONCHANGED:
@ -190,13 +216,29 @@ namespace Observatory.UI
public Guid Guid { get => _guid; }
private void AdjustText()
public void AdjustOffset(bool increase)
{
if (_defaultPosition)
{
if (increase || Location != _originalLocation)
{
var corner = Properties.Core.Default.NativeNotifyCorner;
if ((corner >= 2 && increase) || (corner <= 1 && !increase))
{
Location = new Point(Location.X, Location.Y + Height);
}
else
{
Location = new Point(Location.X, Location.Y - Height);
}
}
}
}
private void CloseNotification(object? sender, System.Timers.ElapsedEventArgs e)
{
// Catch Cross-thread access and invoke
try
{
Close();
@ -205,14 +247,14 @@ namespace Observatory.UI
{
try
{
this.Invoke(() => Close());
Invoke(() => Close());
}
catch
catch (Exception ex)
{
throw new Exception("blah");
throw new Exception("Notification Close Failure, please inform Vithigar. Details: " + ex.Message);
}
}
_timer.Stop();
_timer.Dispose();
}

View File

@ -1,4 +1,64 @@
<root>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,7 +1,14 @@
using Observatory.Framework.Interfaces;
using Observatory.Framework;
using System.Collections;
using Observatory.PluginManagement;
using Observatory.Utils;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Reflection;
using System.Text.Json;
using System.Data.Common;
using System.ComponentModel.Design.Serialization;
namespace Observatory.UI
{
@ -39,6 +46,7 @@ namespace Observatory.UI
Font = menu.Items[0].Font,
TextAlign = menu.Items[0].TextAlign
};
ThemeManager.GetInstance.RegisterControl(newItem);
menu.Items.Add(newItem);
if (plugin.PluginUI.PluginUIType == Framework.PluginUI.UIType.Basic)
@ -49,7 +57,10 @@ namespace Observatory.UI
private static Panel CreateBasicUI(IObservatoryPlugin plugin)
{
Panel panel = new();
Panel panel = new()
{
Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom | AnchorStyles.Top
};
IObservatoryComparer columnSorter;
if (plugin.ColumnSorter != null)
@ -57,7 +68,7 @@ namespace Observatory.UI
else
columnSorter = new DefaultSorter();
ListView listView = new()
PluginListView listView = new()
{
View = View.Details,
Location = new Point(0, 0),
@ -65,16 +76,120 @@ namespace Observatory.UI
Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom | AnchorStyles.Top,
BackColor = Color.FromArgb(64, 64, 64),
ForeColor = Color.LightGray,
GridLines = true,
ListViewItemSorter = columnSorter,
Font = new Font(new FontFamily("Segoe UI"), 10, FontStyle.Regular)
};
foreach (var property in plugin.PluginUI.DataGrid.First().GetType().GetProperties())
string colSize = Properties.Core.Default.ColumnSizing;
List<ColumnSizing>? columnSizing = null;
if (!string.IsNullOrWhiteSpace(colSize))
{
listView.Columns.Add(property.Name);
try
{
columnSizing = JsonSerializer.Deserialize<List<ColumnSizing>>(colSize);
}
catch
{
// Failed deserialization means bad value, blow it away.
Properties.Core.Default.ColumnSizing = string.Empty;
Properties.Core.Default.Save();
}
}
columnSizing ??= new List<ColumnSizing>();
// Is losing column sizes between versions acceptable?
ColumnSizing pluginColumnSizing = columnSizing
.Where(c => c.PluginName == plugin.Name && c.PluginVersion == plugin.Version)
.FirstOrDefault(new ColumnSizing() { PluginName = plugin.Name, PluginVersion = plugin.Version });
if (!columnSizing.Contains(pluginColumnSizing))
{
columnSizing.Add(pluginColumnSizing);
}
foreach (var property in plugin.PluginUI.DataGrid.First().GetType().GetProperties())
{
// https://stackoverflow.com/questions/5796383/insert-spaces-between-words-on-a-camel-cased-token
string columnLabel = Regex.Replace(
Regex.Replace(
property.Name,
@"(\P{Ll})(\P{Ll}\p{Ll})",
"$1 $2"
),
@"(\p{Ll})(\P{Ll})",
"$1 $2"
);
int width;
if (pluginColumnSizing.ColumnWidth.ContainsKey(columnLabel))
{
width = pluginColumnSizing.ColumnWidth[columnLabel];
}
else
{
var widthAttrib = property.GetCustomAttribute<ColumnSuggestedWidth>();
width = widthAttrib == null
// Rough approximation of width by label length if none specified.
? columnLabel.Length * 10
: widthAttrib.Width;
pluginColumnSizing.ColumnWidth.Add(columnLabel, width);
}
listView.Columns.Add(columnLabel, width);
}
Properties.Core.Default.ColumnSizing = JsonSerializer.Serialize(columnSizing);
Properties.Core.Default.Save();
// Oddly, the listview resize event often fires after the column size change but
// with stale (default?!) column width values.
// Still need a resize handler to avoid the ugliness of the rightmost column
// leaving gaps, but preventing saving the width changes there should stop the
// stale resize event from overwriting with bad data.
// Using a higher-order function here to create two different versions of the
// event handler for these purposes.
var handleColSize = (bool saveProps) =>
(object? sender, EventArgs e) =>
{
int colTotalWidth = 0;
ColumnHeader? rightmost = null;
foreach (ColumnHeader column in listView.Columns)
{
colTotalWidth += column.Width;
if (rightmost == null || column.DisplayIndex > rightmost.DisplayIndex)
rightmost = column;
if (saveProps)
{
if (pluginColumnSizing.ColumnWidth.ContainsKey(column.Text))
pluginColumnSizing.ColumnWidth[column.Text] = column.Width;
else
pluginColumnSizing.ColumnWidth.Add(column.Text, column.Width);
}
}
if (rightmost != null && colTotalWidth < listView.Width)
{
rightmost.Width = listView.Width - (colTotalWidth - rightmost.Width);
if (saveProps)
pluginColumnSizing.ColumnWidth[rightmost.Text] = rightmost.Width;
}
if (saveProps)
{
Properties.Core.Default.ColumnSizing = JsonSerializer.Serialize(columnSizing);
Properties.Core.Default.Save();
}
};
listView.ColumnWidthChanged += handleColSize(true).Invoke;
listView.Resize += handleColSize(false).Invoke;
listView.ColumnClick += (sender, e) =>
{
if (e.Column == columnSorter.SortColumn)
@ -99,10 +214,10 @@ namespace Observatory.UI
};
panel.Controls.Add(listView);
plugin.PluginUI.DataGrid.CollectionChanged += (sender, e) =>
{
listView.Invoke(() =>
var updateGrid = () =>
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add &&
e.NewItems != null)
@ -153,7 +268,16 @@ namespace Observatory.UI
listView.Items.Add(listItem);
}
}
});
};
if (listView.Created)
{
listView.Invoke(updateGrid);
}
else
{
updateGrid();
}
};
return panel;

View File

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Text;
using System.Threading.Tasks;
namespace Observatory.UI
{
internal class PluginListView : ListView
{
[DllImport("user32.dll")]
private static extern int SendMessage(IntPtr hWnd, int wMsg, bool wParam, int lParam);
private const int WM_SETREDRAW = 11;
public PluginListView()
{
OwnerDraw = true;
GridLines = false;
DrawItem += PluginListView_DrawItem;
DrawSubItem += PluginListView_DrawSubItem;
DrawColumnHeader += PluginListView_DrawColumnHeader;
DoubleBuffered = true;
base.GridLines = false;//We should prevent the default drawing of gridlines.
}
private static void DrawBorder(Graphics graphics, Pen pen, Rectangle bounds, bool header = false)
{
Point topRight = new(bounds.Right, bounds.Top);
Point bottomRight = new(bounds.Right, bounds.Bottom);
graphics.DrawLine(pen, topRight, bottomRight);
if (header)
{
Point bottomLeft = new(bounds.Left, bounds.Bottom);
// Point topLeft = new(bounds.Left, bounds.Top);
// graphics.DrawLine(pen, topLeft, topRight);
// graphics.DrawLine(pen, topLeft, bottomLeft);
graphics.DrawLine(pen, bottomLeft, bottomRight);
}
}
private void PluginListView_DrawColumnHeader(object? sender, DrawListViewColumnHeaderEventArgs e)
{
using (var g = e.Graphics)
if (g != null)
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Pen pen = new(new SolidBrush(Color.LightGray));
DrawBorder(g, pen, e.Bounds);
using (var font = new Font(this.Font, FontStyle.Bold))
{
Brush textBrush = new SolidBrush(ForeColor);
g.DrawString(e.Header?.Text, font, textBrush, e.Bounds);
}
}
}
private void PluginListView_DrawSubItem(object? sender, DrawListViewSubItemEventArgs e)
{
using (var g = e.Graphics)
if (g != null)
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Pen pen = new(new SolidBrush(Color.LightGray));
DrawBorder(g, pen, e.Bounds, false);
e.DrawText();
}
}
private void PluginListView_DrawItem(object? sender, DrawListViewItemEventArgs e)
{
var offsetColor = (int value) =>
{
if (value > 127)
{
return value - 20;
}
else
{
return value + 20;
}
};
using (var g = e.Graphics)
{
if (e.ItemIndex % 2 == 0)
{
e.Item.BackColor = BackColor;
}
else
{
e.Item.BackColor = Color.FromArgb(offsetColor(BackColor.R), offsetColor(BackColor.G), offsetColor(BackColor.B));
}
if (g != null)
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Pen pen = new(new SolidBrush(Color.LightGray));
e.DrawBackground();
}
}
}
}
}

View File

@ -0,0 +1,84 @@
namespace Observatory.UI
{
partial class ReadAllForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
ReadAllProgress = new ProgressBar();
JournalLabel = new Label();
CancelButton = new Button();
SuspendLayout();
//
// ReadAllProgress
//
ReadAllProgress.Location = new Point(12, 27);
ReadAllProgress.Name = "ReadAllProgress";
ReadAllProgress.Size = new Size(371, 23);
ReadAllProgress.Step = 1;
ReadAllProgress.TabIndex = 0;
//
// JournalLabel
//
JournalLabel.AutoSize = true;
JournalLabel.Location = new Point(12, 9);
JournalLabel.Name = "JournalLabel";
JournalLabel.Size = new Size(45, 15);
JournalLabel.TabIndex = 1;
JournalLabel.Text = "foo.log";
//
// CancelButton
//
CancelButton.Location = new Point(308, 56);
CancelButton.Name = "CancelButton";
CancelButton.Size = new Size(75, 23);
CancelButton.TabIndex = 2;
CancelButton.Text = "Cancel";
CancelButton.UseVisualStyleBackColor = true;
CancelButton.Click += CancelButton_Click;
//
// ReadAllForm
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(395, 86);
Controls.Add(CancelButton);
Controls.Add(JournalLabel);
Controls.Add(ReadAllProgress);
FormBorderStyle = FormBorderStyle.FixedDialog;
Name = "ReadAllForm";
Text = "Read All In Progress...";
ResumeLayout(false);
PerformLayout();
}
#endregion
private ProgressBar ReadAllProgress;
private Label JournalLabel;
private Button CancelButton;
}
}

View File

@ -0,0 +1,52 @@
using Observatory.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Observatory.UI
{
public partial class ReadAllForm : Form
{
private CancellationTokenSource ReadAllCancel;
public ReadAllForm()
{
InitializeComponent();
var ReadAllJournals = LogMonitor.GetInstance.ReadAllGenerator(out int fileCount);
int progressCount = 0;
ReadAllCancel = new CancellationTokenSource();
HandleCreated += (_,_) =>
Task.Run(() =>
{
foreach (var journal in ReadAllJournals())
{
if (ReadAllCancel.IsCancellationRequested)
{
break;
}
progressCount++;
Invoke(() =>
{
JournalLabel.Text = journal.ToString();
ReadAllProgress.Value = (progressCount * 100) / fileCount;
});
}
Invoke(()=>Close());
});
}
private void CancelButton_Click(object sender, EventArgs e)
{
ReadAllCancel.Cancel();
}
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -0,0 +1,61 @@
namespace Observatory.UI
{
partial class SettingsForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
PluginSettingsCloseButton = new Button();
SuspendLayout();
//
// PluginSettingsCloseButton
//
PluginSettingsCloseButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
PluginSettingsCloseButton.Location = new Point(339, 5);
PluginSettingsCloseButton.Name = "PluginSettingsCloseButton";
PluginSettingsCloseButton.Size = new Size(75, 23);
PluginSettingsCloseButton.TabIndex = 0;
PluginSettingsCloseButton.Text = "Close";
PluginSettingsCloseButton.UseVisualStyleBackColor = true;
PluginSettingsCloseButton.Click += PluginSettingsCloseButton_Click;
//
// SettingsForm
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(426, 40);
Controls.Add(PluginSettingsCloseButton);
FormBorderStyle = FormBorderStyle.FixedSingle;
Name = "SettingsForm";
Text = "SettingsForm";
ResumeLayout(false);
}
#endregion
private Button PluginSettingsCloseButton;
}
}

View File

@ -0,0 +1,367 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Observatory.Assets;
using Observatory.Framework;
using Observatory.Framework.Interfaces;
namespace Observatory.UI
{
public partial class SettingsForm : Form
{
private readonly IObservatoryPlugin _plugin;
private readonly List<int> _colHeight = new List<int>();
public SettingsForm(IObservatoryPlugin plugin)
{
InitializeComponent();
_plugin = plugin;
// Filtered to only settings without SettingIgnore attribute
var settings = PluginManagement.PluginManager.GetSettingDisplayNames(plugin.Settings).Where(s => !Attribute.IsDefined(s.Key, typeof(SettingIgnore)));
CreateControls(settings);
Text = plugin.Name + " Settings";
Icon = Resources.EOCIcon_Presized;
ThemeManager.GetInstance.RegisterControl(this);
}
private void CreateControls(IEnumerable<KeyValuePair<PropertyInfo, string>> settings)
{
bool recentHalfCol = false;
int settingsHeight = 0;
var trackBottomEdge = (Control control) =>
{
var controlBottom = control.Location.Y + control.Height;
if (controlBottom > settingsHeight)
settingsHeight = controlBottom;
};
foreach (var setting in settings)
{
// Reset the column tracking for checkboxes if this isn't a checkbox
if (setting.Key.PropertyType.Name != "Boolean")
recentHalfCol = false;
int addedHeight = 29;
switch (setting.Key.GetValue(_plugin.Settings))
{
case bool:
var checkBox = CreateBoolSetting(setting);
addedHeight = recentHalfCol ? 0 : addedHeight;
checkBox.Location = GetSettingPosition(recentHalfCol);
recentHalfCol = !recentHalfCol;
Controls.Add(checkBox);
trackBottomEdge(checkBox);
break;
case string:
var stringLabel = CreateSettingLabel(setting.Value);
var textBox = CreateStringSetting(setting.Key);
stringLabel.Location = GetSettingPosition();
textBox.Location = GetSettingPosition(true);
Controls.Add(stringLabel);
Controls.Add(textBox);
trackBottomEdge(textBox);
break;
case FileInfo:
var fileLabel = CreateSettingLabel(setting.Value);
var pathTextBox = CreateFilePathSetting(setting.Key);
var pathButton = CreateFileBrowseSetting(setting.Key, pathTextBox);
fileLabel.Location = GetSettingPosition();
pathTextBox.Location = GetSettingPosition(true);
_colHeight.Add(addedHeight);
pathButton.Location = GetSettingPosition(true);
Controls.Add(fileLabel);
Controls.Add(pathTextBox);
Controls.Add(pathButton);
trackBottomEdge(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;
if (Attribute.IsDefined(setting.Key, typeof(SettingNumericUseSlider)))
{
intControl = CreateSettingTrackbar(setting.Key);
}
else
{
intControl = CreateSettingNumericUpDown(setting.Key);
}
intLabel.Location = GetSettingPosition();
intControl.Location = GetSettingPosition(true);
addedHeight = intControl.Height;
intLabel.Height = intControl.Height;
intLabel.TextAlign = ContentAlignment.MiddleRight;
Controls.Add(intLabel);
Controls.Add(intControl);
trackBottomEdge(intControl);
break;
case Action action:
var button = CreateSettingButton(setting.Value, action);
button.Location = GetSettingPosition();
Controls.Add(button);
trackBottomEdge(button);
break;
case Dictionary<string, object> dictSetting:
var dictLabel = CreateSettingLabel(setting.Value);
var dropdown = CreateSettingDropdown(setting.Key, dictSetting);
dictLabel.Location = GetSettingPosition();
dropdown.Location = GetSettingPosition(true);
Controls.Add(dictLabel);
Controls.Add(dropdown);
trackBottomEdge(dropdown);
break;
default:
break;
}
_colHeight.Add(addedHeight);
}
Height = settingsHeight + 80;
}
private Point GetSettingPosition(bool secondCol = false)
{
return new Point(10 + (secondCol ? 200 : 0), -26 + _colHeight.Sum());
}
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<string, object> 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;
}
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));
var minBound = Convert.ToInt32(bounds?.Minimum ?? 0);
var maxBound = Convert.ToInt32(bounds?.Maximum ?? 100);
var tickFrequency = maxBound - minBound >= 20 ? (maxBound - minBound) / 10 : 1;
TrackBar trackBar = new()
{
Orientation = Orientation.Horizontal,
TickStyle = TickStyle.Both,
TickFrequency = tickFrequency,
Width = 200,
Minimum = minBound,
Maximum = maxBound,
};
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<PropertyInfo, string> 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 void SaveSettings()
{
PluginManagement.PluginManager.GetInstance.SaveSettings(_plugin, _plugin.Settings);
}
private void PluginSettingsCloseButton_Click(object sender, EventArgs e)
{
Close();
}
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Observatory.UI
{
internal class ThemeManager
{
public static ThemeManager GetInstance
{
get
{
return _instance.Value;
}
}
private static readonly Lazy<ThemeManager> _instance = new(() => new ThemeManager());
private bool _init;
private ThemeManager()
{
_init = true;
controls = new List<Control>();
Themes = new()
{
{ "Dark", DarkTheme },
{ "Light", LightTheme }
};
SelectedTheme = "Dark";
}
private readonly List<Control> controls;
public List<string> GetThemes
{
get => Themes.Keys.ToList();
}
public string CurrentTheme
{
get => SelectedTheme;
set
{
if (Themes.ContainsKey(value))
{
SelectedTheme = value;
foreach (var control in controls)
{
ApplyTheme(control);
}
}
}
}
public void RegisterControl(Control control)
{
// First time registering a control, build the "light" theme based
// on defaults.
if (_init)
{
SaveTheme(control, LightTheme);
_init = false;
}
controls.Add(control);
ApplyTheme(control);
if (control.HasChildren)
foreach (Control child in control.Controls)
{
RegisterControl(child);
}
}
// This doesn't inherit from Control? Seriously?
public void RegisterControl(ToolStripMenuItem toolStripMenuItem)
{
ApplyTheme(toolStripMenuItem);
}
private void SaveTheme(Control control, Dictionary<string, Color> theme)
{
Control rootControl = control;
while (rootControl.Parent != null)
{
rootControl = rootControl.Parent;
}
SaveThemeControl(rootControl, theme);
var themeJson = System.Text.Json.JsonSerializer.Serialize(DarkTheme);
}
private void SaveThemeControl(Control control, Dictionary<string, Color> theme)
{
var properties = control.GetType().GetProperties();
var colorProperties = properties.Where(p => p.PropertyType == typeof(Color));
foreach (var colorProperty in colorProperties)
{
string controlKey = control.GetType().Name + "." + colorProperty.Name;
if (!theme.ContainsKey(controlKey))
{
theme.Add(controlKey, (Color)colorProperty.GetValue(control)!);
}
}
foreach (Control child in control.Controls)
{
SaveThemeControl(child, theme);
}
}
public void DeRegisterControl(Control control)
{
if (control.HasChildren)
foreach (Control child in control.Controls)
{
DeRegisterControl(child);
}
controls.Remove(control);
}
private void ApplyTheme(Object control)
{
var controlType = control.GetType();
var theme = Themes.ContainsKey(SelectedTheme)
? Themes[SelectedTheme] : Themes["Light"];
foreach (var property in controlType.GetProperties().Where(p => p.PropertyType == typeof(Color)))
{
string themeControl = Themes[SelectedTheme].ContainsKey(controlType.Name + "." + property.Name)
? controlType.Name
: "Default";
if (Themes[SelectedTheme].ContainsKey(themeControl + "." + property.Name))
property.SetValue(control, Themes[SelectedTheme][themeControl + "." + property.Name]);
}
}
private Dictionary<string, Dictionary<string, Color>> Themes;
private string SelectedTheme;
private Dictionary<string, Color> LightTheme = new Dictionary<string, Color>();
static private Dictionary<string, Color> DarkTheme = new Dictionary<string, Color>
{
{"Default.ForeColor", Color.LightGray },
{"Default.BackColor", Color.Black },
{"Button.ForeColor", Color.LightGray },
{"Button.BackColor", Color.DimGray }
};
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NAudio.Wave;
namespace Observatory.Utils
{
internal static class AudioHandler
{
internal static async Task PlayFile(string filePath)
{
await Task.Run(() =>
{
using (var file = new AudioFileReader(filePath))
using (var output = new WaveOutEvent())
{
output.Init(file);
output.Play();
while (output.PlaybackState == PlaybackState.Playing)
{
Thread.Sleep(250);
}
};
});
}
}
}

View File

@ -22,7 +22,7 @@ namespace Observatory.Utils
}
}
private static readonly Lazy<LogMonitor> _instance = new Lazy<LogMonitor>(NewLogMonitor);
private static readonly Lazy<LogMonitor> _instance = new(NewLogMonitor);
private static LogMonitor NewLogMonitor()
{
@ -44,6 +44,9 @@ namespace Observatory.Utils
{
get => currentState;
}
public Status Status { get; private set; }
#endregion
#region Public Methods
@ -87,28 +90,31 @@ namespace Observatory.Utils
return LogMonitorStateChangedEventArgs.IsBatchRead(currentState);
}
public void ReadAllJournals()
{
ReadAllJournals(string.Empty);
}
public void ReadAllJournals(string path)
public Func<IEnumerable<string>> ReadAllGenerator(out int fileCount)
{
// Prevent pre-reading when starting monitoring after reading all.
firstStartMonitor = false;
SetLogMonitorState(currentState | LogMonitorState.Batch);
DirectoryInfo logDirectory = GetJournalFolder(path);
DirectoryInfo logDirectory = GetJournalFolder();
var files = GetJournalFilesOrdered(logDirectory);
var readErrors = new List<(Exception ex, string file, string line)>();
foreach (var file in files)
{
readErrors.AddRange(
ProcessLines(ReadAllLines(file.FullName), file.Name));
}
fileCount = files.Count();
ReportErrors(readErrors);
SetLogMonitorState(currentState & ~LogMonitorState.Batch);
IEnumerable<string> ReadAllJournals()
{
var readErrors = new List<(Exception ex, string file, string line)>();
foreach (var file in files)
{
yield return file.Name;
readErrors.AddRange(
ProcessLines(ReadAllLines(file.FullName), file.Name));
}
ReportErrors(readErrors);
SetLogMonitorState(currentState & ~LogMonitorState.Batch);
};
return ReadAllJournals;
}
public void PrereadJournals()
@ -187,13 +193,13 @@ namespace Observatory.Utils
#region Private Fields
private FileSystemWatcher journalWatcher;
private FileSystemWatcher statusWatcher;
private Dictionary<string, Type> journalTypes;
private Dictionary<string, int> currentLine;
private FileSystemWatcher? journalWatcher;
private FileSystemWatcher? statusWatcher;
private readonly Dictionary<string, Type> journalTypes;
private readonly Dictionary<string, int> currentLine;
private LogMonitorState currentState = LogMonitorState.Idle; // Change via #SetLogMonitorState
private bool firstStartMonitor = true;
private string[] EventsWithAncillaryFile = new string[]
private readonly string[] EventsWithAncillaryFile = new string[]
{
"Cargo",
"NavRoute",
@ -242,7 +248,7 @@ namespace Observatory.Utils
statusWatcher.Changed += StatusUpdateEvent;
}
private DirectoryInfo GetJournalFolder(string path = "")
private static DirectoryInfo GetJournalFolder(string path = "")
{
DirectoryInfo logDirectory;
@ -370,7 +376,7 @@ namespace Observatory.Utils
}
}
private void ReportErrors(List<(Exception ex, string file, string line)> readErrors)
private static void ReportErrors(List<(Exception ex, string file, string line)> readErrors)
{
if (readErrors.Any())
{
@ -410,24 +416,22 @@ namespace Observatory.Utils
}
catch (Exception ex)
{
ReportErrors(new List<(Exception ex, string file, string line)>() { (ex, eventArgs.Name, line) });
ReportErrors(new List<(Exception ex, string file, string line)>() { (ex, eventArgs.Name ?? string.Empty, line) });
}
}
currentLine[eventArgs.FullPath] = fileContent.Count;
}
private List<string> ReadAllLines(string path)
private static List<string> ReadAllLines(string path)
{
var lines = new List<string>();
try
{
using (StreamReader file = new StreamReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
using StreamReader file = new(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
while (!file.EndOfStream)
{
while (!file.EndOfStream)
{
lines.Add(file.ReadLine());
}
lines.Add(file.ReadLine() ?? string.Empty);
}
}
catch (IOException ioEx)
@ -450,6 +454,7 @@ namespace Observatory.Utils
if (statusLines.Count > 0)
{
var status = JournalReader.ObservatoryDeserializer<Status>(statusLines[0]);
Status = status;
handler?.Invoke(this, new JournalEventArgs() { journalType = typeof(Status), journalEvent = status });
}
}
@ -486,9 +491,9 @@ namespace Observatory.Utils
IntPtr pathPtr = IntPtr.Zero;
try
{
Guid FolderSavedGames = new Guid("4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4");
Guid FolderSavedGames = new ("4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4");
SHGetKnownFolderPath(ref FolderSavedGames, 0, IntPtr.Zero, out pathPtr);
return Marshal.PtrToStringUni(pathPtr);
return Marshal.PtrToStringUni(pathPtr) ?? string.Empty;
}
finally
{
@ -496,7 +501,7 @@ namespace Observatory.Utils
}
}
private IEnumerable<FileInfo> GetJournalFilesOrdered(DirectoryInfo journalFolder)
private static IEnumerable<FileInfo> GetJournalFilesOrdered(DirectoryInfo journalFolder)
{
return from file in journalFolder.GetFiles("Journal.*.??.log")
orderby file.LastWriteTime