Processing Ajax...

Title
Close Dialog

Message

Confirm
Close Dialog

Confirm
Close Dialog

Automatically Span Window Across Two Monitors

Description
Finds the two best monitors to span a window across based on alignment and resolution then spans it across them.
Language
C#.net
Minimum Version
Created By
Derek Ziemba
Contributors
-
Date Created
Mar 12, 2020
Date Last Modified
Mar 10, 2022

Scripted Function (Macro) Code

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

}