using System;
using System.Drawing;
// Finds the two best monitors to span a window across based on alignment and resolution then spans it across them.
// * Saves the previous position (lifetime of window), so 2nd click of the assign title button restores previous position
// * Doesn't care if your monitors switched ids because of a driver update, etc - which breaks DisplayFusions regular Custom Functions
// * Works over remote Desktop. The built in Custom Functions never get alignment right when there's any kind of monitor mismatch.
// * Only restores previous position if that looks like what's intended
// ex: If you accidentally move the window slightly, it will instead re-doublewide the window.
// Written by Derek Ziemba
// PS: Any plans to update the DisplayFusion compiler? The lack of modern features tripped me up a bit.
public static class DisplayFusionFunction {
private const string KeyPrefix = "DoubleWide_";
private const string KeyLastUsedDate = KeyPrefix + "LastUsedDate";
// ref was intentionally used instead of 'out'. If it fails I don't want to overwrite the result, because in this use case that rectangle is the doublewide dimensions we still may want to apply
private static void LoadPriorSize(IntPtr handle, ref Rectangle result) {
string prevstr = BFS.ScriptSettings.ReadValue(KeyPrefix + handle.ToInt32().ToString());
if (!String.IsNullOrWhiteSpace(prevstr)) {
var arr = prevstr.Split(',');
// TryParse intentionally avoided. If an error occurs here, I'd like to know why something didn't work.
result = new Rectangle(Int32.Parse(arr[0]), Int32.Parse(arr[1]), Int32.Parse(arr[2]), Int32.Parse(arr[3]));
}
// Attempt to do some garbage collection
// I don't want my registery becoming litered with irrelevant entires
DateTime dateLastInvoked = default(DateTime);
if (DateTime.TryParse(BFS.ScriptSettings.ReadValue(KeyLastUsedDate), out dateLastInvoked)) {
if (dateLastInvoked.AddHours(24) < DateTime.UtcNow) {
// Am assuming this only deletes entries related to this script
// A way to specify a LifeTimePolicy for saved values would be nice. Like on next restart, x days, etc.
BFS.ScriptSettings.DeleteAllValues();
return;
}
}
}
private static void SaveCurrentSize(IntPtr handle, Rectangle rect) {
BFS.ScriptSettings.WriteValue(KeyLastUsedDate, DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm"));
var data = String.Join(",", new Int32[] { rect.X, rect.Y, rect.Width, rect.Height });
// The handle is used to make the keys unique.
// Titles, like shown in examples, can change depending on what webpage your on.
// But the handle should be consistent for the lifetime of the window.
BFS.ScriptSettings.WriteValue(KeyPrefix + handle.ToInt32().ToString(), data);
}
private static bool AreSimilarSizeAndAlignment(ref Rectangle a, ref Rectangle b, Int32 sizeSlop, Int32 ySlop, Int32 xSlop) {
return Math.Abs(a.Width - b.Width) <= sizeSlop && Math.Abs(a.Height - b.Height) <= sizeSlop && Math.Abs(a.Y - b.Y) <= ySlop && Math.Abs(a.X - b.X) <= xSlop;
}
public static void Run(IntPtr windowHandle) {
if (windowHandle == IntPtr.Zero) { return; }
Rectangle[] monitors = BFS.Monitor.GetMonitorWorkAreas();
if (monitors.Length < 2) {
BFS.Dialog.ShowMessageError("Requires at least 2 monitors.");
return;
}
MonitorPair pair = new MonitorPair();
// Try to find the best pair of monitors to span across. Slowly relax the requirements when a suitable pair can't be found
bool found = pair.InitializePair(ref monitors, 5, 5) || pair.InitializePair(ref monitors, 20, 35) || pair.InitializePair(ref monitors, 25, 50) || pair.InitializePair(ref monitors, 300, 300);
if (!found) {
BFS.Dialog.ShowMessageError("No Monitors of similar size and alignment found in consecutive horizontal order.\nSpanning a window across them would look dumb and not be practical.\n" + monitors.Inspect(", "));
return;
}
//BFS.Dialog.ShowMessageInfo(monitors.Inspect(", "));
//BFS.Dialog.ShowMessageInfo(pair.Inspect());
Rectangle current = BFS.Window.GetBounds(windowHandle);
Rectangle target = pair.ToRect();
if (AreSimilarSizeAndAlignment(ref current, ref target, 4, 4, 4)) {
// We're already pretty much fullsize, so they want to undo doublewide
LoadPriorSize(windowHandle, ref target); // if the call
BFS.Window.SetSizeAndLocation(windowHandle, target.X, target.Y, target.Width, target.Height);
} else if (AreSimilarSizeAndAlignment(ref current, ref target, 20, 300, 300)) {
// User may have accidentally moved the window and just wants it to be full double size again
// So Don't save/overwrite whatever size it's currently at
BFS.Window.SetSizeAndLocation(windowHandle, pair.X, pair.Y, pair.Width, pair.Height);
} else {
SaveCurrentSize(windowHandle, current);
BFS.Window.SetSizeAndLocation(windowHandle, pair.X, pair.Y, pair.Width, pair.Height);
}
}
private struct MonitorPair {
public Rectangle First;
public Rectangle Second;
public Int32 X { get { return Math.Min(this.First.X, this.Second.X); } }
public Int32 Y { get { return Math.Max(this.First.Y, this.Second.Y); } }
public Int32 Top { get { return Math.Min(this.First.Top, this.Second.Top); } }
public Int32 Bottom { get { return Math.Max(this.First.Bottom, this.Second.Bottom); } }
public Int32 Width { get { return this.Second.Right - this.First.Left; } }
public Int32 Height { get { return this.Bottom - this.Top; } }
public bool InitializePair(ref Rectangle[] monitors, Int32 sizeSlop, Int32 alignmentSlop) {
this.First = monitors[0];
for (var i = 1; i < monitors.Length; i++) {
this.Second = monitors[i];
if (AreSimilarSizeAndAlignment(ref First, ref Second, sizeSlop, alignmentSlop, alignmentSlop + Second.Height)) {
return true;
}
this.First = this.Second;
}
return false;
}
public Rectangle ToRect() { return new Rectangle(this.X, this.Y, this.Width, this.Height); }
public override string ToString() { return this.Inspect(); }
}
private static string Inspect<T>(this T input) {
var sb = new System.Text.StringBuilder("{ ", 128);
var props = typeof(T).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.GetProperty);
foreach (var prop in props) {
if (prop.PropertyType.IsPrimitive) { sb.Append("\n ").Append(prop.Name).Append(": ").Append(prop.GetValue(input)).Append(", "); }
}
sb.Remove(sb.Length - 2, 2);
return sb.Append("\n}").ToString();
}
private static string Inspect<T>(this T[] input, string separator) {
var ls = new System.Collections.Generic.List<string>(input.Length);
foreach (var value in input) { ls.Add(value.Inspect()); }
return String.Join(separator, ls);
}
}