Processing Ajax...

Title

Message

Confirm

Confirm

Confirm

Confirm

Are you sure you want to delete this item?

Confirm

Are you sure you want to delete this item?

Confirm

Are you sure?

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

}