diff --git a/.gitignore b/.gitignore
index 27b5c94..eba8177 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,4 @@ _ReSharper*/
.vs/
#Nuget packages folder
packages/
+/design/
diff --git a/KeePassTrayIconLockState/KeePassTrayIconLockState.csproj b/KeePassTrayIconLockState/KeePassTrayIconLockState.csproj
index 4fad490..f418bed 100644
--- a/KeePassTrayIconLockState/KeePassTrayIconLockState.csproj
+++ b/KeePassTrayIconLockState/KeePassTrayIconLockState.csproj
@@ -49,6 +49,11 @@
+
+ True
+ True
+ Resources.resx
+
@@ -56,5 +61,23 @@
2.0.18.1
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/KeePassTrayIconLockState/KeePassTrayIconLockStateExt.cs b/KeePassTrayIconLockState/KeePassTrayIconLockStateExt.cs
index 4c1dd0b..b7f8a13 100644
--- a/KeePassTrayIconLockState/KeePassTrayIconLockStateExt.cs
+++ b/KeePassTrayIconLockState/KeePassTrayIconLockStateExt.cs
@@ -6,70 +6,80 @@
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
-using KeePass.App;
using KeePass.Plugins;
using KeePass.Resources;
-using KeePass.UI;
using KoKo.Property;
namespace KeePassTrayIconLockState {
// ReSharper disable once UnusedType.Global
- public class KeePassTrayIconLockStateExt: Plugin, IDisposable {
+ public class KeePassTrayIconLockStateExt: Plugin {
internal static readonly TimeSpan STARTUP_DURATION = TimeSpan.FromMilliseconds(2000);
- internal static readonly TimeSpan ANIMATION_PERIOD = TimeSpan.FromMilliseconds(700);
private readonly IDictionary iconsByFileOpenState = new Dictionary();
- private readonly StoredProperty isDatabaseOpen = new StoredProperty(false);
+ private readonly StoredProperty databaseOpenState = new StoredProperty(DatabaseOpenState.CLOSED);
private IPluginHost keePassHost = null!;
- private Timer? animationTimer;
- private Property trayIcon = null!;
+ private Property trayIcon = null!;
public KeePassTrayIconLockStateExt() {
- Icon lockedIcon = loadIcon(false);
- Icon unlockedIcon = loadIcon(true);
-
- iconsByFileOpenState.Add(DatabaseOpenState.CLOSED, lockedIcon);
- iconsByFileOpenState.Add(DatabaseOpenState.OPENING, lockedIcon);
- iconsByFileOpenState.Add(DatabaseOpenState.OPEN, unlockedIcon);
+ iconsByFileOpenState.Add(DatabaseOpenState.CLOSED, Resources.locked);
+ iconsByFileOpenState.Add(DatabaseOpenState.OPENING, Resources.unlocking);
+ iconsByFileOpenState.Add(DatabaseOpenState.OPEN, Resources.unlocked);
}
public override bool Initialize(IPluginHost host) {
keePassHost = host;
- keePassHost.MainWindow.FileOpened += delegate { isDatabaseOpen.Value = true; };
- keePassHost.MainWindow.FileClosed += delegate { isDatabaseOpen.Value = false; };
-
- ToolStripItem statusBarInfo = keePassHost.MainWindow.Controls.OfType().First().Items[1];
- Property statusBarText = new NativeReadableProperty(statusBarInfo, nameof(ToolStripItem.Text), nameof(ToolStripItem.TextChanged));
+ keePassHost.MainWindow.FileOpened += delegate { databaseOpenState.Value = DatabaseOpenState.OPEN; };
+ keePassHost.MainWindow.FileClosed += delegate { databaseOpenState.Value = DatabaseOpenState.CLOSED; };
+
+ ToolStripItemCollection statusBarItems = keePassHost.MainWindow.Controls.OfType().First().Items;
+ ToolStripItem statusBarInfo = statusBarItems[1];
+ ToolStripItem progressBar = statusBarItems[2];
+ Property statusBarText = new NativeReadableProperty(statusBarInfo, nameof(ToolStripItem.Text), nameof(ToolStripItem.TextChanged));
+ Property isProgressBarVisible = new NativeReadableProperty(progressBar, nameof(ToolStripItem.Visible), nameof(ToolStripItem.VisibleChanged));
+
+ statusBarText.PropertyChanged += (sender, args) => {
+ if (args.NewValue == KPRes.OpeningDatabase2) {
+ /*
+ * When the status bar text changes to "Opening database...", set the db open state to OPENING.
+ */
+ databaseOpenState.Value = DatabaseOpenState.OPENING;
+ }
+ };
- Property databaseOpenState = DerivedProperty.Create(isDatabaseOpen, statusBarText, (isOpen, statusText) => {
- if (statusText == KPRes.OpeningDatabase2) {
- return DatabaseOpenState.OPENING;
- } else {
- return isOpen ? DatabaseOpenState.OPEN : DatabaseOpenState.CLOSED;
+ isProgressBarVisible.PropertyChanged += (sender, args) => {
+ if (!args.NewValue && databaseOpenState.Value == DatabaseOpenState.OPENING) {
+ /*
+ * When the database is being opened and the progress bar gets hidden, it means the database was finished being decrypted, but was it successful or unsuccessful?
+ * Sadly there is no good way to tell, so instead we wait 100 ms for the FileOpen event to be fired.
+ * If it is fired, the db open state moves from OPENING to OPEN.
+ * Otherwise, assume that the database failed to decrypt and set the db open state to CLOSED.
+ */
+ Task.Delay(100).ContinueWith(task => {
+ if (databaseOpenState.Value == DatabaseOpenState.OPENING) { // failed to decrypt, otherwise this would have been set to OPEN by the FileOpen event above.
+ databaseOpenState.Value = DatabaseOpenState.CLOSED;
+ }
+ });
+
+ } else if (args.NewValue && databaseOpenState.Value == DatabaseOpenState.CLOSED && statusBarText.Value == KPRes.OpeningDatabase2) {
+ /*
+ * When the database is closed and the status bar says "Opening database..." and the progress bar is shown, it means the user already failed the previous decryption attempt
+ * and is retrying after submitting another password, so set the db opening state to OPENING.
+ */
+ databaseOpenState.Value = DatabaseOpenState.OPENING;
}
- });
+ };
- trayIcon = DerivedProperty.Create(databaseOpenState, isOpen => new TrayIcon(iconsByFileOpenState[isOpen], isOpen != DatabaseOpenState.CLOSED));
+ trayIcon = DerivedProperty.Create(databaseOpenState, openState => new TrayIcon(iconsByFileOpenState[openState], openState != DatabaseOpenState.CLOSED));
trayIcon.PropertyChanged += (sender, args) => renderTrayIcon();
- animationTimer = new Timer { Enabled = false, Interval = (int) ANIMATION_PERIOD.TotalMilliseconds };
- bool animationShowClosedIcon = false;
- animationTimer.Tick += delegate {
- keePassHost.MainWindow.MainNotifyIcon.Icon = iconsByFileOpenState[animationShowClosedIcon ? DatabaseOpenState.CLOSED : DatabaseOpenState.OPEN];
- animationShowClosedIcon ^= true;
- };
-
- databaseOpenState.PropertyChanged += (sender, args) => {
- animationShowClosedIcon = false;
- animationTimer.Enabled = args.NewValue == DatabaseOpenState.OPENING;
- };
-
- // Give KeePass time to stop setting its own icon
+ /*
+ * KeePass sets its own icon at some indeterminate time after startup, so repeatedly set our own icon every 8 ms for 2 seconds to make sure our icon isn't overridden.
+ */
Timer startupTimer = new Timer { Enabled = true, Interval = 8 };
startupTimer.Tick += delegate { renderTrayIcon(); };
Task.Delay(STARTUP_DURATION).ContinueWith(task => startupTimer.Stop());
@@ -78,26 +88,15 @@ public override bool Initialize(IPluginHost host) {
return true;
}
- private static Icon loadIcon(bool fileOpen) {
- AppIconType appIconType = fileOpen ? AppIconType.QuadNormal : AppIconType.QuadLocked;
- return AppIcons.Get(appIconType, UIUtil.GetSmallIconSize(), Color.Empty);
- }
-
private void renderTrayIcon() {
- TrayIcon icon = trayIcon.Value;
- keePassHost.MainWindow.MainNotifyIcon.Icon = icon.image;
- keePassHost.MainWindow.MainNotifyIcon.Visible = icon.isVisible;
- }
-
- public override Image SmallIcon => iconsByFileOpenState[DatabaseOpenState.CLOSED].ToBitmap();
+ TrayIcon iconToRender = trayIcon.Value;
+ NotifyIcon keepassIcon = keePassHost.MainWindow.MainNotifyIcon;
- public override void Terminate() {
- Dispose();
+ keepassIcon.Icon = iconToRender.image;
+ keepassIcon.Visible = iconToRender.isVisible;
}
- public void Dispose() {
- animationTimer?.Dispose();
- }
+ public override Image SmallIcon => Resources.plugin_image;
}
diff --git a/KeePassTrayIconLockState/Resources.Designer.cs b/KeePassTrayIconLockState/Resources.Designer.cs
new file mode 100644
index 0000000..492e4f9
--- /dev/null
+++ b/KeePassTrayIconLockState/Resources.Designer.cs
@@ -0,0 +1,103 @@
+//------------------------------------------------------------------------------
+//
+// 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.
+//
+//------------------------------------------------------------------------------
+
+namespace KeePassTrayIconLockState {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // 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", "16.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() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [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("KeePassTrayIconLockState.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
+ ///
+ internal static System.Drawing.Icon locked {
+ get {
+ object obj = ResourceManager.GetObject("locked", resourceCulture);
+ return ((System.Drawing.Icon)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Bitmap.
+ ///
+ internal static System.Drawing.Bitmap plugin_image {
+ get {
+ object obj = ResourceManager.GetObject("plugin_image", resourceCulture);
+ return ((System.Drawing.Bitmap)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
+ ///
+ internal static System.Drawing.Icon unlocked {
+ get {
+ object obj = ResourceManager.GetObject("unlocked", resourceCulture);
+ return ((System.Drawing.Icon)(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
+ ///
+ internal static System.Drawing.Icon unlocking {
+ get {
+ object obj = ResourceManager.GetObject("unlocking", resourceCulture);
+ return ((System.Drawing.Icon)(obj));
+ }
+ }
+ }
+}
diff --git a/KeePassTrayIconLockState/Resources.resx b/KeePassTrayIconLockState/Resources.resx
new file mode 100644
index 0000000..6c7922b
--- /dev/null
+++ b/KeePassTrayIconLockState/Resources.resx
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ Resources\locked.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ Resources\plugin image.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ Resources\unlocked.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
+ resources\unlocking.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+
\ No newline at end of file
diff --git a/KeePassTrayIconLockState/Resources/locked.ico b/KeePassTrayIconLockState/Resources/locked.ico
new file mode 100644
index 0000000..b21666b
Binary files /dev/null and b/KeePassTrayIconLockState/Resources/locked.ico differ
diff --git a/KeePassTrayIconLockState/Resources/plugin image.png b/KeePassTrayIconLockState/Resources/plugin image.png
new file mode 100644
index 0000000..690302f
Binary files /dev/null and b/KeePassTrayIconLockState/Resources/plugin image.png differ
diff --git a/KeePassTrayIconLockState/Resources/unlocked.ico b/KeePassTrayIconLockState/Resources/unlocked.ico
new file mode 100644
index 0000000..bd2a9fc
Binary files /dev/null and b/KeePassTrayIconLockState/Resources/unlocked.ico differ
diff --git a/KeePassTrayIconLockState/Resources/unlocking.ico b/KeePassTrayIconLockState/Resources/unlocking.ico
new file mode 100644
index 0000000..bec0276
Binary files /dev/null and b/KeePassTrayIconLockState/Resources/unlocking.ico differ
diff --git a/Test/KeePassTrayIconLockStateExtTest.cs b/Test/KeePassTrayIconLockStateExtTest.cs
index 06b8a79..eafca16 100644
--- a/Test/KeePassTrayIconLockStateExtTest.cs
+++ b/Test/KeePassTrayIconLockStateExtTest.cs
@@ -4,20 +4,20 @@
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
-using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
-using KeePass.App;
+using FluentAssertions;
using KeePass.Forms;
using KeePass.Plugins;
-using KeePass.UI;
using KeePassTrayIconLockState;
using Telerik.JustMock;
using Xunit;
namespace Test {
- public class KeePassTrayIconLockStateExtTest: IDisposable {
+ public class KeePassTrayIconLockStateExtTest {
+
+ private static readonly ImageConverter IMAGE_CONVERTER = new ImageConverter();
private readonly KeePassTrayIconLockStateExt plugin = new KeePassTrayIconLockStateExt();
@@ -26,14 +26,7 @@ public class KeePassTrayIconLockStateExtTest: IDisposable {
private readonly NotifyIcon mainNotifyIcon;
private readonly ToolStripStatusLabel statusPartInfo;
- private readonly TimeSpan loadDelay = TimeSpan.FromTicks(KeePassTrayIconLockStateExt.STARTUP_DURATION.Ticks * 2);
- private readonly TimeSpan animationDelay = KeePassTrayIconLockStateExt.ANIMATION_PERIOD;
- private readonly Icon smallLockedIcon = AppIcons.Get(AppIconType.QuadLocked, UIUtil.GetSmallIconSize(), Color.Empty);
- private readonly Icon smallUnlockedIcon = AppIcons.Get(AppIconType.QuadNormal, UIUtil.GetSmallIconSize(), Color.Empty);
-
- public void Dispose() {
- plugin.Dispose();
- }
+ private readonly TimeSpan loadDelay = TimeSpan.FromTicks(KeePassTrayIconLockStateExt.STARTUP_DURATION.Ticks * 2);
public KeePassTrayIconLockStateExtTest() {
Assert.True(Mock.IsProfilerEnabled, "These tests require the JustMock Profiler to be enabled because the " +
@@ -41,7 +34,7 @@ public KeePassTrayIconLockStateExtTest() {
keePassHost = Mock.Create();
mainWindow = Mock.Create();
- mainNotifyIcon = Mock.Create();
+ mainNotifyIcon = new NotifyIcon();
Control.ControlCollection mainWindowControls = Mock.Create();
StatusStrip statusStrip = new StatusStrip();
@@ -56,68 +49,57 @@ public KeePassTrayIconLockStateExtTest() {
}
[Fact]
- public async void renderBrieflyAfterLoad() {
+ public void startup() {
plugin.Initialize(keePassHost);
- Mock.AssertSet(() => mainNotifyIcon.Icon = smallLockedIcon, Occurs.Never());
- Mock.AssertSet(() => mainNotifyIcon.Visible = false, Occurs.Never());
-
- await Task.Delay(loadDelay);
-
- Mock.AssertSet(() => mainNotifyIcon.Icon = smallLockedIcon, Occurs.Once());
- Mock.AssertSet(() => mainNotifyIcon.Visible = false, Occurs.Once());
+ mainNotifyIcon.Visible.Should().BeFalse();
+ assertIconsEqual(mainNotifyIcon.Icon, Resources.locked);
}
[Fact]
- public async void animateWhileDecrypting() {
+ public async void decrypting() {
plugin.Initialize(keePassHost);
await Task.Delay(loadDelay);
- Mock.AssertSet(() => mainNotifyIcon.Icon = smallLockedIcon, Occurs.Once(), "icon should start locked");
- Mock.AssertSet(() => mainNotifyIcon.Visible = false, Occurs.Once(), "icon should start invisible");
statusPartInfo.Text = "Opening database...";
- await Task.Delay((int) (animationDelay.TotalMilliseconds / 2));
-
- Mock.AssertSet(() => mainNotifyIcon.Icon = smallUnlockedIcon, Occurs.Once(), "immediately after opening, icon should be unlocked");
- Mock.AssertSet(() => mainNotifyIcon.Visible = true, Occurs.Once(), "immediately after opening, icon should be visible");
-
- await Task.Delay(animationDelay);
-
- Mock.AssertSet(() => mainNotifyIcon.Icon = smallLockedIcon, Occurs.Exactly(2));
-
- await Task.Delay(animationDelay);
-
- Mock.AssertSet(() => mainNotifyIcon.Icon = smallUnlockedIcon, Occurs.Exactly(2));
-
- await Task.Delay(animationDelay);
-
- Mock.AssertSet(() => mainNotifyIcon.Icon = smallLockedIcon, Occurs.Exactly(3));
-
- Mock.AssertSet(() => mainNotifyIcon.Visible = false, Occurs.Never());
+ mainNotifyIcon.Visible.Should().BeTrue();
+ assertIconsEqual(mainNotifyIcon.Icon, Resources.unlocking);
}
[Fact]
- public async void unlockAfterOpeningFile() {
+ public async void unlocked() {
plugin.Initialize(keePassHost);
await Task.Delay(loadDelay);
Mock.Raise(() => mainWindow.FileOpened += null, new FileOpenedEventArgs(null));
- Mock.AssertSet(() => mainNotifyIcon.Icon = smallUnlockedIcon, Occurs.Once());
- Mock.AssertSet(() => mainNotifyIcon.Visible = true, Occurs.Once());
+ mainNotifyIcon.Visible.Should().BeTrue();
+ assertIconsEqual(mainNotifyIcon.Icon, Resources.unlocked);
}
[Fact]
- public async void lockAfterClosingFile() {
+ public async void locked() {
plugin.Initialize(keePassHost);
await Task.Delay(loadDelay);
Mock.Raise(() => mainWindow.FileOpened += null, new FileOpenedEventArgs(null));
Mock.Raise(() => mainWindow.FileClosed += null, new FileClosedEventArgs(null, FileEventFlags.Locking));
- Mock.AssertSet(() => mainNotifyIcon.Icon = smallLockedIcon, Occurs.Exactly(2));
- Mock.AssertSet(() => mainNotifyIcon.Visible = false, Occurs.Exactly(2));
+ mainNotifyIcon.Visible.Should().BeFalse();
+ assertIconsEqual(mainNotifyIcon.Icon, Resources.locked);
+ }
+
+ internal static void assertIconsEqual(Icon actual, Icon expected) {
+ getIconBytes(actual.ToBitmap()).Should().Equal(getIconBytes(expected.ToBitmap()));
+ }
+
+ internal static void assertIconsEqual(Image actual, Icon expected) {
+ getIconBytes(actual).Should().Equal(getIconBytes(expected.ToBitmap()));
+ }
+
+ private static IEnumerable? getIconBytes(Image image) {
+ return (byte[]?) IMAGE_CONVERTER.ConvertTo(image, typeof(byte[]));
}
}
diff --git a/Test/PluginTest.cs b/Test/PluginTest.cs
index b1a8bd2..3501bc8 100644
--- a/Test/PluginTest.cs
+++ b/Test/PluginTest.cs
@@ -1,12 +1,9 @@
#nullable enable
using System.Diagnostics;
-using System.Drawing;
using System.IO;
using FluentAssertions;
-using KeePass.App;
using KeePass.Plugins;
-using KeePass.UI;
using KeePassTrayIconLockState;
using Xunit;
@@ -16,7 +13,6 @@ public class PluginTest {
private readonly KeePassTrayIconLockStateExt plugin = new KeePassTrayIconLockStateExt();
- private readonly Icon smallLockedIcon = AppIcons.Get(AppIconType.QuadLocked, UIUtil.GetSmallIconSize(), Color.Empty);
[Fact]
public void derivesFromPluginSuperclass() {
@@ -49,10 +45,7 @@ public void isProductNameSetCorrectly() {
[Fact]
public void pluginIcon() {
- var imageConverter = new ImageConverter();
- var actual = (byte[]) imageConverter.ConvertTo(plugin.SmallIcon, typeof(byte[]));
- var expected = (byte[]) imageConverter.ConvertTo(smallLockedIcon.ToBitmap(), typeof(byte[]));
- actual.Should().Equal(expected);
+ KeePassTrayIconLockStateExtTest.assertIconsEqual(plugin.SmallIcon, Resources.locked);
}
}