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

@ -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 }
};
}
}