Skip to content

Commit

Permalink
While unlocking the database, animate the tray icon (blink the lock i…
Browse files Browse the repository at this point in the history
…con on or off every 0.7 seconds). When starting up, retry hiding the locked tray icon every 8 ms for 2 seconds because KeePass will sometimes re-show the tray icon after we've hidden it.
  • Loading branch information
Aldaviva committed Nov 1, 2020
1 parent 9df27f9 commit 62a6bdc
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 14 deletions.
25 changes: 16 additions & 9 deletions KeePassTrayIconLockState/KeePassTrayIconLockStateExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ namespace KeePassTrayIconLockState {
// ReSharper disable once UnusedType.Global
public class KeePassTrayIconLockStateExt: Plugin, IDisposable {

private readonly TimeSpan startupDelay = TimeSpan.FromMilliseconds(60);
internal static readonly TimeSpan STARTUP_DURATION = TimeSpan.FromMilliseconds(2000);
internal static readonly TimeSpan ANIMATION_PERIOD = TimeSpan.FromMilliseconds(700);

private readonly IDictionary<DatabaseOpenState, Icon> iconsByFileOpenState = new Dictionary<DatabaseOpenState, Icon>();
private readonly StoredProperty<bool> isDatabaseOpen = new StoredProperty<bool>(false);

private IPluginHost keePassHost = null!;
private Timer? animationTimer;
private IPluginHost keePassHost = null!;
private Timer? animationTimer;
private Property<TrayIcon> trayIcon = null!;

public KeePassTrayIconLockStateExt() {
Icon lockedIcon = loadIcon(false);
Expand Down Expand Up @@ -50,11 +53,11 @@ public override bool Initialize(IPluginHost host) {
}
});

Property<TrayIcon> trayIcon = DerivedProperty<TrayIcon>.Create(databaseOpenState, isOpen => new TrayIcon(iconsByFileOpenState[isOpen], isOpen != DatabaseOpenState.CLOSED));
trayIcon = DerivedProperty<TrayIcon>.Create(databaseOpenState, isOpen => new TrayIcon(iconsByFileOpenState[isOpen], isOpen != DatabaseOpenState.CLOSED));

trayIcon.PropertyChanged += (sender, args) => renderTrayIcon(args.NewValue);
trayIcon.PropertyChanged += (sender, args) => renderTrayIcon();

animationTimer = new Timer { Enabled = false, Interval = 600 };
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];
Expand All @@ -66,8 +69,11 @@ public override bool Initialize(IPluginHost host) {
animationTimer.Enabled = args.NewValue == DatabaseOpenState.OPENING;
};

Task.Delay(startupDelay)
.ContinueWith(_ => renderTrayIcon(trayIcon.Value)); // Give KeePass time to stop setting its own icon
// Give KeePass time to stop setting its own icon
Timer startupTimer = new Timer { Enabled = true, Interval = 8 };
startupTimer.Tick += delegate { renderTrayIcon(); };
Task.Delay(STARTUP_DURATION).ContinueWith(task => startupTimer.Stop());
renderTrayIcon();

return true;
}
Expand All @@ -77,7 +83,8 @@ private static Icon loadIcon(bool fileOpen) {
return AppIcons.Get(appIconType, UIUtil.GetSmallIconSize(), Color.Empty);
}

private void renderTrayIcon(TrayIcon icon) {
private void renderTrayIcon() {
TrayIcon icon = trayIcon.Value;
keePassHost.MainWindow.MainNotifyIcon.Icon = icon.image;
keePassHost.MainWindow.MainNotifyIcon.Visible = icon.isVisible;
}
Expand Down
3 changes: 3 additions & 0 deletions KeePassTrayIconLockState/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following
Expand Down Expand Up @@ -33,3 +34,5 @@
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.1.0")]
[assembly: AssemblyFileVersion("1.0.1.0")]

[assembly: InternalsVisibleTo("Test")]
33 changes: 28 additions & 5 deletions Test/KeePassTrayIconLockStateExtTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

namespace Test {

public class KeePassTrayIconLockStateExtTest {
public class KeePassTrayIconLockStateExtTest: IDisposable {

private readonly KeePassTrayIconLockStateExt plugin = new KeePassTrayIconLockStateExt();

Expand All @@ -26,10 +26,15 @@ public class KeePassTrayIconLockStateExtTest {
private readonly NotifyIcon mainNotifyIcon;
private readonly ToolStripStatusLabel statusPartInfo;

private readonly TimeSpan loadDelay = TimeSpan.FromMilliseconds(50 * 2);
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();
}

public KeePassTrayIconLockStateExtTest() {
Assert.True(Mock.IsProfilerEnabled, "These tests require the JustMock Profiler to be enabled because the " +
"MainForm class's constructor crashes if you just run it");
Expand Down Expand Up @@ -64,14 +69,32 @@ public async void renderBrieflyAfterLoad() {
}

[Fact]
public async void showWhileDecrypting() {
public async void animateWhileDecrypting() {
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...";

Mock.AssertSet(() => mainNotifyIcon.Icon = smallLockedIcon, Occurs.Exactly(2));
Mock.AssertSet(() => mainNotifyIcon.Visible = true, Occurs.Once());
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());
}

[Fact]
Expand Down

0 comments on commit 62a6bdc

Please sign in to comment.