Skip to content

Commit

Permalink
Automatic repositioning of the tray context menu (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
dannoe committed Apr 6, 2020
1 parent 8ca4b1c commit c4b54a2
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 1 deletion.
119 changes: 119 additions & 0 deletions KeeTrayTOTP.Tests/TrayContextMenuPositionTests.cs
@@ -0,0 +1,119 @@
using System.Collections.Generic;
using System.Drawing;
using KeeTrayTOTP.Libraries;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace KeeTrayTOTP.Tests
{
[TestClass]
public class TrayContextMenuPositionTests
{
[DataTestMethod]
[DynamicData(nameof(DropDownLocationTestData))]
public void CalculatedLocationForDropDown_Should_ReturnTheCorrectLocation(Size controlSize, Point mousePosition,
Rectangle screenRect, Point expectedPoint)
{
var dropDownLocationCalculator = new DropDownLocationCalculator(controlSize);
var sut = dropDownLocationCalculator.CalculateLocationForDropDown(mousePosition, screenRect);

Assert.AreEqual(expectedPoint, sut);
}

private static IEnumerable<object[]> DropDownLocationTestData
{
get
{
var controlSize = new Size(300, 300);
var screenRect = new Rectangle(0, 0, 1920, 1080);

// top-left
yield return new object[]
{
controlSize, new Point(400, 500), screenRect,
// expected
new Point(400 - controlSize.Width, 500 - controlSize.Height)
};

// top-right
yield return new object[]
{
controlSize, new Point(200, 500), screenRect,
// expected
new Point(200, 500 - controlSize.Height)
};

// bottom-right
yield return new object[]
{
controlSize, new Point(200, 200), screenRect,
// expected
new Point(200, 200)
};

// bottom-left
yield return new object[]
{
controlSize, new Point(400, 200), screenRect,
// expected
new Point(400 - controlSize.Width, 200)
};

// top-left
yield return new object[]
{
controlSize, new Point(300, 300), screenRect,
// expected
new Point(0, 0)
};

// top-left
yield return new object[]
{
controlSize, new Point(screenRect.Width, screenRect.Height), screenRect,
// expected
new Point(screenRect.Width - controlSize.Width, screenRect.Height - controlSize.Height)
};

// edge case control size bigger than screen size
yield return new object[]
{
controlSize, new Point(0, 0), new Rectangle(0, 0, 200, 200),
// expected
new Point(0, 200 - controlSize.Height)
};

// edge case control height bigger than screen height
yield return new object[]
{
controlSize, new Point(50, 0), new Rectangle(0, 0, 1920, 200),
// expected
new Point(50, 200 - controlSize.Height)
};

// edge case control width bigger than screen width
yield return new object[]
{
controlSize, new Point(0, 0), new Rectangle(0, 0, 200, 1080),
// expected
new Point(0, 0)
};

// multi monitor test
yield return new object[]
{
controlSize, new Point(2500, 500), screenRect,
// expected
new Point(2500 - controlSize.Width, 500 - controlSize.Height)
};

// multi monitor test control bigger than screen
yield return new object[]
{
controlSize, new Point(1200, 150), new Rectangle(1080, 0, 200, 200),
// expected
new Point(1080, 200 - controlSize.Height)
};
}
}
}
}
1 change: 1 addition & 0 deletions KeeTrayTOTP/KeeTrayTOTP.csproj
Expand Up @@ -73,6 +73,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Libraries\DropDownLocationCalculator.cs" />
<Compile Include="FormAbout.cs">
<SubType>Form</SubType>
</Compile>
Expand Down
120 changes: 120 additions & 0 deletions KeeTrayTOTP/Libraries/DropDownLocationCalculator.cs
@@ -0,0 +1,120 @@
using System.Drawing;
using System.Windows.Forms;

namespace KeeTrayTOTP.Libraries
{
/// <summary>
/// Calculator for the location of a dropdown control based on the location of the mouse and screen space available
/// </summary>
public class DropDownLocationCalculator
{
private Size _controlSize;

/// <summary>
/// Initializes a calculator which can calculate the location of a dropdown control based on the location of the mouse
/// and the screen space available.
/// </summary>
/// <param name="controlSize">The size of the control which will be dropdown-ed</param>
public DropDownLocationCalculator(Size controlSize)
{
_controlSize = controlSize;
}

/// <summary>
/// Calculate the position for a dropdown control based on the given mouse position.
/// The default behavior is to calculate the location for opening the dropdown to the top-left.
/// </summary>
/// <param name="mousePosition">The position in which the control will be anchored</param>
/// <returns>The new location for the control</returns>
internal Point CalculateLocationForDropDown(Point mousePosition)
{
var screen = Screen.FromPoint(mousePosition);
return CalculateLocationForDropDown(mousePosition, screen.Bounds);
}

/// <summary>
/// Calculate the position for a dropdown control based on the given mouse position and screen rectangle
/// The default behavior is to calculate the location for opening the dropdown to the top-left.
/// </summary>
/// <param name="mousePosition">The position in which the control will be anchored</param>
/// <param name="screenRect">
/// The screen rectangle which is used for the calculation. This should be the screen on which the
/// control will be opened
/// </param>
/// <returns></returns>
internal Point CalculateLocationForDropDown(Point mousePosition, Rectangle screenRect)
{
var screenHeight = screenRect.Height;
var screenWidth = screenRect.Width;

// after calculating the offset, the mouse position is relative to the screen containing the cursor
// this allows for correct calculations even on multi-monitor systems
mousePosition.Offset(-screenRect.X, -screenRect.Y);

int calculatedX = CalculateX(mousePosition, screenWidth);
int calculatedY = CalculateY(mousePosition, screenHeight);

return new Point(calculatedX + screenRect.X, calculatedY + screenRect.Y);
}

private int CalculateY(Point mousePosition, int screenHeight)
{
int calculatedY;
if (IsEnoughSpaceToTheTop(mousePosition))
{
calculatedY = mousePosition.Y - _controlSize.Height;
}
else if (IsEnoughSpaceToTheBottom(mousePosition, screenHeight))
{
calculatedY = mousePosition.Y;
}
else
{
// the control height is higher than the screen height
calculatedY = screenHeight - _controlSize.Height;
}

return calculatedY;
}

private int CalculateX(Point mousePosition, int screenWidth)
{
int calculatedX;
if (IsEnoughSpaceToTheLeft(mousePosition))
{
calculatedX = mousePosition.X - _controlSize.Width;
}
else if (IsEnoughSpaceToTheRight(mousePosition, screenWidth))
{
calculatedX = mousePosition.X;
}
else
{
// the control width is bigger than the screen width
calculatedX = 0;
}

return calculatedX;
}

private bool IsEnoughSpaceToTheLeft(Point mousePosition)
{
return (mousePosition.X - _controlSize.Width) >= 0;
}

private bool IsEnoughSpaceToTheTop(Point mousePosition)
{
return (mousePosition.Y - _controlSize.Height) >= 0;
}

private bool IsEnoughSpaceToTheRight(Point mousePosition, int screenWidth)
{
return (mousePosition.X + _controlSize.Width) <= screenWidth;
}

private bool IsEnoughSpaceToTheBottom(Point mousePosition, int screenHeight)
{
return (mousePosition.Y + _controlSize.Height) <= screenHeight;
}
}
}
12 changes: 11 additions & 1 deletion KeeTrayTOTP/TrayTOTP_Plugin.cs
@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Drawing;
using System.Windows.Forms;
Expand Down Expand Up @@ -271,6 +271,8 @@ public override bool Initialize(IPluginHost host)
_niMenuSeperator = new ToolStripSeparator();
PluginHost.MainWindow.TrayContextMenu.Items.Insert(1, _niMenuSeperator);

PluginHost.MainWindow.TrayContextMenu.Opened += OnTrayContextMenuOpened;

// Register auto-type function.
if (PluginHost.CustomConfig.GetBool(setname_bool_AutoType_Enable, true))
{
Expand All @@ -295,6 +297,13 @@ public override bool Initialize(IPluginHost host)
return true;
}

private void OnTrayContextMenuOpened(object sender, EventArgs e)
{
var contextMenuStrip = (ContextMenuStrip)sender;
var dropDownLocationCalculator = new DropDownLocationCalculator(contextMenuStrip.Size);
contextMenuStrip.Location = dropDownLocationCalculator.CalculateLocationForDropDown(Cursor.Position);
}

/// <summary>
/// Occurs when the main window is shown.
/// </summary>
Expand Down Expand Up @@ -913,6 +922,7 @@ public override void Terminate()

// Remove Notify Icon menus.
PluginHost.MainWindow.TrayContextMenu.Opening -= OnNotifyMenuOpening;
PluginHost.MainWindow.TrayContextMenu.Opened -= OnTrayContextMenuOpened;
PluginHost.MainWindow.TrayContextMenu.Items.Remove(_niMenuTitle);
_niMenuTitle.Dispose();
foreach (var menu in _niMenuList)
Expand Down

0 comments on commit c4b54a2

Please sign in to comment.