Processing Ajax...

Title
Close Dialog

Message

Confirm
Close Dialog

Confirm
Close Dialog

Confirm
Close Dialog

Project Setup Opener

Description
I'm a software developer for construction machinery. When working on a project I like to have multiple Explorer Windows open which are related to the project. And of course I like to have them positioned over my multiple monitor setup.
So wrote this scripted function which allows to store a setup of open Explorer windows for a given parent window.
Use hotkey 'Alt + Ctrl + Win + Y' while the parent window is focused.
Both the paths and the positions of open explorer windows are stored for the current monitor configuration.
If later on the parent window is opened while the spacebar is pressed, the complete setup is restored.
Individual setups can be stored for different monitor configurations. If a setup does not exist for
the current monitor configuration, the script applies one of the existing ones and assures that
no window is outside the visible area.
You need to assign:
- 'Alt + Ctrl + Win + Y' as hotkey to run this scripted function
- A trigger for:
- 'Window created',
- 'Process Filename': 'C:\Windows\explorer.exe',
- 'Windows Class': 'CabinetWClass'
- 'Run Function': 'Project Setup Opener'
If you want to change the hotkey 'Alt + Ctrl + Win + Y', or want a different key than the spacebar to restore setups, make sure to adjust the strings 'KeyDownOpener', 'KeyDownOpener2' and 'KeyDownCreator' in the script to your choices.
See the comment section at the beginning of the script for details.
Language
C#.net
Minimum Version
Created By
Christian Treffler
Contributors
-
Date Created
Nov 23, 2021
Date Last Modified
May 1, 2023

Scripted Function (Macro) Code

using System;
using System.Net;
using System.IO;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Serialization;
using System.Text;
using System.Drawing;
using System.Web;
using System.Linq;
using System.Runtime.InteropServices;

// V1.1
// by: Christian Treffler
// 
// The function allows to store a setup of open Explorer windows for a given parent window.
// Use hotkey 'Alt + Ctrl + Win + Y' while the parent window is focused.
// Both the paths and the positions of these windows are stored for the current monitor configuration.
// If the parent window is opened while the spacebar is pressed, the complete setup is restored.
// Individual setups can be stored for different monitor configurations. If a setup does not exist for 
// the current monitor configuration, the script applies one of the existing ones and assures that
// no window is outside the visible area.
// A kind of recursion could occur, but is per default prevented in the code: 
// If one of the windows which are opened for a parent folder is itself a parent folder for a project setup,
// it would cause the respective setup to be executed, too.
// If you want to allow this recursion, set 'PreventRecursion' below to 'false'.
//
// You need to assign:
// - 'Alt + Ctrl + Win + Y' as hotkey to run this scripted function
// - A trigger for:
//   - 'Window created', 
//   - 'Process Filename': 'C:\Windows\explorer.exe', 
//   - 'Windows Class': 'CabinetWClass'
//   - 'Run Function': 'Project Setup Opener'
//
// If you want to change the hotkey 'Alt + Ctrl + Win + Y', or want a different key than the spacebar to 
// restore setups, make sure to adjust the strings 'KeyDownOpener', 'KeyDownOpener2' and 'KeyDownCreator' below to your choices
//
// History:
// V1.1: 
//      - z-Order of Windows (Which one is above others) is now taken into account when storing setups
//    - Helpers for Debugging and Logging
//    - Bugfix: Check, if item is null in ExplorerWindows.InitExplorerHandler
//    - Bugfix: Wait 500ms before getting Explorer-Windows. Otherwise the path to the windows might be empty
//    - Bugfix: Increase wait time from 200ms to 500ms between opening windows and moving them. Otherwise some won't move.
//    - Bugfix: Build hash for stored setup from full path, not directory name. it can happen that different paths have the same directry name
//    - InitExplorerHandler: Replaced Parallel.For loop with a single thread for loop. Suspicion of threads not beeing stopped.
// V1.2:
//    - Sometimes some Windows still don't move to their place
//       - Add a one time repeat of window moving after 'allpresent == true'
//       - decrease wait time while iteration to speed it up
// V1.3:
//    - Bugfix: The decrease did not work because of 'if (waittime > 500)'
//    - Bugfix:
//      GetHashCode does not work as expected anymore since DisplayFusion 10.0
//      See https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/ for details and fix
//    - Only run the script, if windowHandle belongs to an Explorer window
//    - Recursion prevention very early in the script 
//    - Move parent window immediately, so that user has feedback and can release the space bar
//    - Prevent invalid state
//    - Having the class ExplorerWindows as static causes lots of problems. Renamed to ExplorerWindowsHandler 
//      and created an instance of the class in the main function instead
//    - Prevent storage of empty logs
//    - Improve logging


public static class DisplayFusionFunction
{
    // Virtual Key Codes: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
    
    // When this key is pressed while opening an explorer window, 
    // the function will check, if a project setup exists and apply it
    private const string KeyDownOpener = "32";       // Spacebar 
    private const string KeyDownOpener2 = "18;32";   // Ctrl + Spacebar. This is for opening a folder with ctrl - doubleclick and space

    // When this hotkey combination is pressed, the function will store the current project setup
    // This string must be identical to the key combination used to start this function
    private const string KeyDownCreator = "17;18;89;91"; // Alt + Ctrl + Win + Y
    
    private const bool PreventRecursion = true;  // Set to false, if recursion should be allowed
    private const int  BlockTime        = 8000;  // Block recursion for <BlockTime> ms. 
    private const int  WaitTimeStart    = 400;   // Wait before final loading of currently open Explorer windows. 
    private const int  WaitTimeMove     = 300;   // Wait before moving recently opened windows. 
    private const int  MoveCountMax     = 10;    // Max number of trials to open windows
    
    private const bool ErrorMessages    = false; // Set to true for debugging Purposes 
    private const bool Logging          = false; // Set to true for for additional Logging Messages 
    
    public static void Run(IntPtr windowHandle)
    {
        if(BFS.Window.GetClass(windowHandle) != "CabinetWClass") return; // V1.3 Only run the script, if windowHandle belongs to an Explorer window

        LogFile LogToFile = new LogFile();
        LogToFile.ErrorMessages = ErrorMessages;
        LogToFile.Log = Logging;

        try
        {
            ProjectSetups SavedSetups = null;
            ProjectSetup  LoadedSetup = null;
            string currPath = "";
            string currDirectory = "";
            string ProfileName = "";
            uint trialtime = WaitTimeStart;
            ExplorerWindowsHandler ExplorerWindows = new ExplorerWindowsHandler();

            bool opener  =  BFS.Input.IsKeyDown(KeyDownOpener)
                         || BFS.Input.IsKeyDown(KeyDownOpener2); // Folder opened while Spacebar is pressed?
            bool creator = BFS.Input.IsKeyDown(KeyDownCreator);  // Started by Hotkey?
            
            if (opener && creator) // V1.3: Prevent invalid state
            {
                opener  = false;
                creator = false;
                LogToFile.AddMessage("Invalid");
            }
            
            if (opener && PreventRecursion)
                {
                    int timeblocked = BFS.ScriptSettings.ReadValueInt("PSO_TimeBlocked");
                    int timenow = Environment.TickCount;
                    
                    LogToFile.AddMessage("Prevent Recursion", true);
                    
                    if (timeblocked > timenow) // System probably restarted, timeblocked is not valid
                    {
                        timeblocked = 0;
                        LogToFile.AddMessage("Probable Restart");
                    }
                    
                    if (timeblocked + BlockTime > timenow)
                    {
                        opener = false;
                        LogToFile.AddMessage("Blocked: " + timeblocked.ToString() + "; " + timenow.ToString());
                    }
                    else
                    {
                        BFS.ScriptSettings.WriteValueInt("PSO_TimeBlocked", timenow);
                        LogToFile.AddMessage("PSO_TimeBlocked: " + timenow.ToString());
                    }
                    
                    LogToFile.SetIndent(false);
                }
            
            ExplorerWindows.ErrorMessages = ErrorMessages;
                                    
            // V1.3: Move parent window immediately, so that user has feedback and can release the space bar
            if (opener || creator)    // Standard initialization for all purposes
            {
                ExplorerWindows.LogToFile = LogToFile;
                LogToFile.AddMessage("Initialization", true);
                while ((trialtime > 100) && (currPath.Length == 0))
                {
                    ExplorerWindows.InitExplorerHandler(windowHandle);
                    currPath = ExplorerWindows.GetPath(windowHandle, true);    // Path to current folder which is the parent for the Project Setup 
                    if(currPath.Length == 0)
                    {
                        trialtime = trialtime - 100;
                        BFS.General.ThreadWait(100);
                    }
                }
                LogToFile.SetIndent(false);
                
                if(currPath.Length > 0)
                {
                    try
                    {
                        LogToFile.AddMessage("Path: " + currPath);
                        LogToFile.AddMessage("Remaining Waitime: " + trialtime.ToString(), true);
                        currDirectory = Path.GetFileName(currPath);                    // Name of the current folder
						LogToFile.AddMessage("Load Setup: " + currPath.GetDeterministicHashCode() + " - " + currDirectory + ".xml");
                        SavedSetups = LoadProjectSetups(currDirectory, currPath);   // Check, if project setup exists (otherwise empty setup)
                        // Get Resolution of all Monitors and create profile name
                        Rectangle[] Monitors = BFS.Monitor.GetMonitorBounds(); 
                        ProfileName = buildProfileName(Monitors);
                        LogToFile.AddMessage("SavedSetups: " + SavedSetups.SetupName + "; " + SavedSetups.HasSetups().ToString());
                        
                        // Move parent window
                        string path = ExplorerWindows.GetPath(windowHandle, false);
                        LoadedSetup = SavedSetups.GetSetup(ProfileName);        
                        Rectangle rect = LoadedSetup.Prgrms[path];
                        BFS.Window.SetSizeAndLocation(windowHandle, rect.Left, rect.Top, rect.Width, rect.Height);
                        LogToFile.AddMessage("Parent Window: " + windowHandle.ToString() + ": " + rect.ToString());
                    }
                    catch (Exception e)
                    {
                        if (ErrorMessages) BFS.Dialog.ShowMessageError("Parent:\n" + e.ToString());
                        LogToFile.AddMessage("Exception in Parent: " + e.ToString());
                    }
                }
                else
                {
                    opener  = false;
                    creator = false;
                    LogToFile.AddMessage("Abort", false);
                }
            }
            
            if (opener || creator)    
            {
                // Get all open Explorer windows and set relevant variables
                BFS.General.ThreadWait(trialtime); // Bugfix V1.1. Otherwise the paths to windows might be empty
                ExplorerWindows.InitExplorerHandler(windowHandle);
                LogToFile.SetIndent(false);
            }
            
            if (opener)         // Apply Project Setup
            {
                LogToFile.AddMessage("Opener", true);
                
                if (    opener
                    &&  SavedSetups.HasSetups())
                {
                    LogToFile.AddMessage("Load Setup", true);
                    LoadedSetup = SavedSetups.GetSetup(ProfileName);
                    
                    // Open windows, if they are not already open
                    foreach (KeyValuePair<string, Rectangle> window in LoadedSetup.Prgrms) 
                    {
                        LogToFile.AddMessage("Handle: " + HttpUtility.UrlDecode(window.Key, System.Text.Encoding.Default));
                        IntPtr handle = ExplorerWindows.GetHandle(window.Key, false);
                        if (handle == IntPtr.Zero)
                        {
                            uint appid = BFS.Application.Start(HttpUtility.UrlDecode(window.Key, System.Text.Encoding.Default));
                            LogToFile.AddMessage("Start Application");
                        }
                    }
                                        
                    bool allpresent = false;
                    bool onetimerepeat = false;
                    int  counter = 0;
                    uint waittime = WaitTimeMove;
					uint delta = WaitTimeMove / MoveCountMax;
                    List <IntPtr> allwindows = new List <IntPtr>();
                    
                    // It seems that there is a considerable time between 'BFS.Application.Start' and the actual existence of the 
                    // respective explorer window. In order to speed up, the delay time is cut short and we try several times until
                    // all windows are present
                    while ( ((allpresent == false) || (onetimerepeat == false)) && (counter < MoveCountMax) )
                    {
                        // Allow all windows to open 
                        // V1.1: 200ms seemed to be too short in the past, 300ms did not work either. Try 500ms
                        // V1.2: Start with 'waittime = 500', then decrease every iteration
                        // V1.3: Bugfix: The decrease did not work because of 'if (waittime > 500)'
                        BFS.General.ThreadWait(waittime);    
                        if (waittime > ((100 > delta) ? 100 : delta))
                        {
                            waittime = waittime - delta;
                        }
                        
                        onetimerepeat = allpresent; // V1.2: If all windows are present now, repeat one more time
                        
                        allpresent = true;
                        counter++;
                        
                        LogToFile.AddMessage("While loop: " + allpresent.ToString() + "; " + onetimerepeat.ToString() + "; " + counter.ToString(), true);
                        ExplorerWindows.LogToFile = LogToFile;
                        ExplorerWindows.InitExplorerHandler(windowHandle);
                        
                        // Move the windows to their positions
                        foreach (KeyValuePair<string, Rectangle> window in LoadedSetup.Prgrms) 
                        {
                            IntPtr handle = ExplorerWindows.GetHandle(window.Key, true);
                            if (handle != IntPtr.Zero)
                            {
                                if (!allwindows.Contains(handle))
                                {
                                    allwindows.Add(handle);
                                    Rectangle rect = window.Value;
                                    BFS.Window.SetSizeAndLocation(handle, rect.Left, rect.Top, rect.Width, rect.Height);
                                    LogToFile.AddMessage(window.Key + "; " + handle.ToString() + ": " + rect.ToString());
                                }
                            }
                            else
                            {
                                allpresent = false;
                                LogToFile.AddMessage(window.Key + ": missing");
                            }
                        }
                        LogToFile.SetIndent(false);
                    }
                    
                    BFS.General.ThreadWait(1000);    // Allow all windows to find their place                    
                    
                    // Find windows which are not on a visible screen
                    LogToFile.AddMessage("Check off screen", true);
                    foreach (IntPtr handle in allwindows)
                    {
                        Rectangle WinRect = BFS.Window.GetBounds(handle);
                        bool moveit = false; 
                        uint monitorid = BFS.Monitor.GetMonitorIDByWindow(handle);
                        if(monitorid == 0)    // Not on any monitor
                        {
                            moveit = true;
                        }
                        else
                        {
                            Rectangle IntersectSize = Rectangle.Intersect(WinRect, BFS.Monitor.GetMonitorBoundsByID(monitorid));
                            if (IntersectSize.Width < 3 || IntersectSize.Height < 3) // Have a monitor, but barely visible
                            {
                                moveit = true;
                            }
                        }
                        
                        LogToFile.AddMessage(handle.ToString() + ", moveit: " + moveit.ToString());
                        
                        if(moveit)
                        {
                            BFS.DisplayFusion.RunFunctionWithWindowHandle("Move Window to Current Monitor", handle);
                            BFS.Window.MoveToCentreMonitor(handle);
                            BFS.Window.SetSize(handle, WinRect.Width, WinRect.Height);
                        }
                    }
                    LogToFile.SetIndent(false);
                    LogToFile.SetIndent(false);
                }
                LogToFile.SetIndent(false);
            }
            else if (creator)   // Save Project Setup
            {
                LogToFile.AddMessage("Creator", true);
                ProjectSetup newsetup = new ProjectSetup();
                IntPtr[] handles = BFS.Window.GetAllWindowHandles() ;
                
                foreach (IntPtr handle in handles)
                {
                    string path = ExplorerWindows.GetPath(handle, false);
                    if (path != "")
                    {
                        int order = ExplorerWindows.GetOrder(handle);
                        newsetup.AddProgram(path, handle, order);
                        LogToFile.AddMessage(handle.ToString() + ": " + order.ToString() + ", " + path);
                    }
                }
                
                newsetup.SortProgram();
                SavedSetups.AddSetup(ProfileName, newsetup);
                SavedSetups.ProjectPath = currPath;
                string SetupName = SaveData(SavedSetups, currDirectory, currPath);
                BFS.Dialog.ShowMessageInfo("Stored Setup: " + currDirectory);
                LogToFile.AddMessage("Stored Setup: " + currDirectory);
                LogToFile.AddMessage("Setup File: " + SetupName);
            }
        }
        catch (Exception e)
        {
            if (ErrorMessages) BFS.Dialog.ShowMessageError("Run:\n" + e.ToString());
            LogToFile.AddMessage("Exception in Run: " + e.ToString());
        }
        LogToFile.StoreLog();
    }
	
    // From https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/
	static int GetDeterministicHashCode(this string str)
	{
	    unchecked
	    {
	        int hash1 = (5381 << 16) + 5381;
	        int hash2 = hash1;
	
	        for (int i = 0; i < str.Length; i += 2)
	        {
	            hash1 = ((hash1 << 5) + hash1) ^ str[i];
	            if (i == str.Length - 1)
	                break;
	            hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
	        }
	
	        return hash1 + (hash2 * 1566083941);
	    }
	}

    public static string SaveData(ProjectSetups Data, string setupName, string setupPath) // store all window settings in an xml file with the provided filename
    {
        XmlSerializer XmlSer = new XmlSerializer(typeof(ProjectSetups));         // Provides the methods for XML-Serialization
        // will be stored in the users local DisplayFusion AppData folder:
        string destination = Environment.ExpandEnvironmentVariables("%userprofile%\\AppData\\Local\\DisplayFusion");
        string p = "";
		string sname = "";
        bool ready2copy = false; // Copy only if destination folder exists
        
        try
        {
            if (Directory.Exists(destination))  // DisplyFusion user folder found?
            {
                // Creat a subfolder if it doesn't exist, yet
                destination = destination + "\\ProjectSetups";
                ready2copy = Directory.Exists(destination);
                if (!ready2copy)
                {
                    ready2copy = Directory.CreateDirectory(destination).Exists;
                }
            }
			sname = setupPath.GetDeterministicHashCode() + " - " + setupName + ".xml";
            p = destination + "\\" + sname; // Full Filename with path
        }
        
        catch (Exception e) 
        {
            if (ErrorMessages) BFS.Dialog.ShowMessageError("SaveProgramSettings:\n" + e.ToString());
			ready2copy = false;
			sname = "";
        }
            
        if (ready2copy) // if destination folder exists
        {
            FileStream DStream = new FileStream(p, FileMode.Create);     // Create the Filestream, overwrite mode
           
            try
            {
                XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); // Use this object
                ns.Add("", "");                                             // to prevent that attributes are added in the files

                XmlWriterSettings xws = new XmlWriterSettings();            // Use this object to
                xws.Indent = true;                                          // - add line feeds and indents
                xws.CloseOutput = true;                                     // - close stream after writing
                xws.Encoding = Encoding.Default;                            // - set encoding
                xws.IndentChars = "      ";                                 // - set indent depth
                xws.NewLineHandling = NewLineHandling.None;                 // - handle line feeds properly

                using (XmlWriter writer = XmlWriter.Create(DStream, xws))   // Now serialize
                {
                    XmlSer.Serialize(writer, Data, ns);
                    writer.Close();
                }
            }

            catch (Exception e) 
            {
                if (ErrorMessages) BFS.Dialog.ShowMessageError("SaveProgramSettings_2:\n" + e.ToString());
				sname = "";
            }

            finally
            {
                if (!(DStream == null)) DStream.Close();                    // Close the stream
            }       
        }
		
		return sname;
    }

    public static ProjectSetups LoadProjectSetups(string setupName, string setupPath)   
    {
        ProjectSetups r = new ProjectSetups();
        XmlSerializer XmlSer = new XmlSerializer(typeof(ProjectSetups)); // Provides the methods for XML-Serialization
        
        string destination = Environment.ExpandEnvironmentVariables("%userprofile%\\AppData\\Local\\DisplayFusion");
        string p = "";
        bool ready2copy = false; // Copy only if destination folder exists
        bool success = false;
        string sname = setupPath.GetDeterministicHashCode() + " - " + setupName + ".xml";
        
        if (Directory.Exists(destination))  // DisplyFusion user folder found?
        {
            // Creat a subfolder, if it doesn't exist, yet
            destination = destination + "\\ProjectSetups";
            ready2copy = Directory.Exists(destination);
            if (!ready2copy)
            {
                ready2copy = Directory.CreateDirectory(destination).Exists;
            }
        }
        
        p = destination + "\\" + sname; // Full Filename with path

        FileStream DStream = null;
        try
        {
            if (File.Exists(p))
            {
                DStream = new FileStream(p, FileMode.Open);         // Create the Filestream
                r = (ProjectSetups)XmlSer.Deserialize(DStream);     // and load the Data 
                success = true;
            }
        }

        catch (Exception e) 
        {
            if (ErrorMessages) BFS.Dialog.ShowMessageError("LoadProjectSetups:\n" + e.ToString());
            success = false;
        }

        finally
        {
            if (!(DStream == null)) DStream.Close();                // Done
            if (r == null) 
            {
                r = new ProjectSetups();   
            }
            else if (success == true)
            {
                 r.SetupName = sname;
            }
        }
        return r;
    }

    public static string buildProfileName(Rectangle[] screens)
    {
        // needs System.Text to have StringBuilder class        
        StringBuilder pname = new StringBuilder();
        
        for (int i = 0; i < screens.Length; i++) // For each monitor
        {
            // Add monitors resolution in the format "[<Width>x<Height>, <Y>]"
            pname.Append("[");
            pname.Append(screens[i].Width);
            pname.Append("x");
            pname.Append(screens[i].Height);
            if(screens[i].X!=0 || screens[i].Y!=0) // Primary screen has coordinates [0,0]
            {
                // Add the y-coordinate, if not primary screen
                pname.Append(",");
                pname.Append(screens[i].Y);
            }
            pname.Append("]");
        }
        
        return pname.ToString();
    }

    [Serializable]
    public class ProjectSetups     // list of ProjectSetups per Monitor Konfiguration
    {
        public ProjectSetups() { } // empty constructor is needed to make the class serializable

        public string ProjectPath = "";
        public string SetupName = "";
        public XMLDictionary<string, ProjectSetup> Setups = new XMLDictionary<string, ProjectSetup>();  // list of Programs

        public void AddSetup(string monconfig, ProjectSetup setup)  
        {
            if(Setups.ContainsKey(monconfig))
            {
                Setups[monconfig] = setup;
            }
            else
            {
                Setups.Add(monconfig, setup);
            }
        }
        
        public bool HasSetups()
        {
            if (Setups.Count > 0) return true;
            return false;
        }
        
        public ProjectSetup GetSetup(string monconfig)
        {
            if(Setups.ContainsKey(monconfig))
            {
                return Setups[monconfig];
            }
            else
            {
                return Setups.First().Value;
            }
        }
    }

    [Serializable]
    public class ProjectSetup      // list of window settings
    {
        public ProjectSetup() { }  // empty constructor is needed to make the class serializable

        private SortedList<int, IntPtr> SortedPointer = new SortedList<int, IntPtr>();
        private Dictionary<IntPtr, string> WindowNames = new Dictionary<IntPtr, string>();
        public XMLDictionary<string, Rectangle> Prgrms = new XMLDictionary<string, Rectangle>();  // list of Programs

        public void AddProgram(string name, IntPtr handle, int zorder)  
        {
            SortedPointer.Add(zorder, handle);
            WindowNames.Add(handle, name);
        }
        
        public void SortProgram()  
        {
            foreach(KeyValuePair<int, IntPtr> kvp in SortedPointer)
            {
                IntPtr newhandle = kvp.Value;
                string newname   = WindowNames[newhandle];
                if(Prgrms.ContainsKey(newname))
                {
                    Prgrms[newname] = BFS.Window.GetBounds(newhandle);
                }
                else
                {
                    Prgrms.Add(newname, BFS.Window.GetBounds(newhandle));
                }
            }
        }
    }
}


// ***** XMLDictionary *****
// A Module to provide a dictionary which is serializable.
// Purpose: e.g. ini-files
// Paul Welter 3/5/2006
//
// http://weblogs.asp.net/pwelter34/archive/2006/05/03/444961.aspx
public class XMLDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
{
    public System.Xml.Schema.XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(System.Xml.XmlReader reader)
    {
        XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));

        bool wasEmpty = reader.IsEmptyElement;
        reader.Read();

        if (wasEmpty)
            return;

        while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
        {
            reader.ReadStartElement("item");

            reader.ReadStartElement("key");
            TKey key = (TKey)keySerializer.Deserialize(reader);
            reader.ReadEndElement();

            reader.ReadStartElement("value");
            TValue value = (TValue)valueSerializer.Deserialize(reader);
            reader.ReadEndElement();

            this.Add(key, value);

            reader.ReadEndElement();
            reader.MoveToContent();
        }
        reader.ReadEndElement();
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
        XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));

        // Addition by Christian Treffler, 6/9/2010:
        XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); // Use this object
        ns.Add("", "");                                             // to prevent that attributes are added in the files
        // End of addition

        foreach (TKey key in this.Keys)
        {
            writer.WriteStartElement("item");

            writer.WriteStartElement("key");
            keySerializer.Serialize(writer, key, ns);       // Added ns in this call
            writer.WriteEndElement();

            writer.WriteStartElement("value");
            TValue value = this[key];
            valueSerializer.Serialize(writer, value, ns);   // Added ns in this call
            writer.WriteEndElement();

            writer.WriteEndElement();
        }
    }
}

public class LogFile     // Logging for Debugging
{
    public DateTime StartTime = DateTime.Now;
    public List<string> Messages = new List<string>();
    public bool Log = false;
    public bool ErrorMessages = false;
    private int IndentLevel = 0;
    
    public void AddMessage(string message)
    {
        if (Log)
        {
            Messages.Add(new string(' ', IndentLevel * 3) + message);
        }
    }
    
    public void AddMessage(string message, bool indent)
    {
        if (Log)
        {
            AddMessage(message);
            SetIndent(indent);
        }
    }
    
    public void SetIndent (bool indent)
    {
        if (Log)
        {
            if (indent)
            {
                IndentLevel++;
            }
            else
            {
                if (IndentLevel > 0)
                {
                    IndentLevel--;
                }
            }
        }
    }

    
    public void StoreLog()
    {
        // will be stored in the users local DisplayFusion AppData folder:
        string destination = Environment.ExpandEnvironmentVariables("%userprofile%\\AppData\\Local\\DisplayFusion");
        string p = "";
        bool ready2copy = false; // Copy only if destination folder exists
        
        if (Log && (Messages.Count > 0))
        {
            try
            {
                if (Directory.Exists(destination))  // DisplyFusion user folder found?
                {
                    // Creat a subfolder if it doesn't exist, yet
                    destination = destination + "\\Logs";
                    ready2copy = Directory.Exists(destination);
                    if (!ready2copy)
                    {
                        ready2copy = Directory.CreateDirectory(destination).Exists;
                    }
                }

                p = destination + "\\" + StartTime.ToString("yyMMdd_HH_mm_ss_fffff") + " PSO.log"; // Full Filename with path
            }
            catch (Exception e) 
            {
                ready2copy = false;
                if (ErrorMessages) BFS.Dialog.ShowMessageError("StoreLog, destination:\n" + e.ToString());
            }
                   
            if (ready2copy) // if destination folder exists
            {
                // from: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/file-system/how-to-write-to-a-text-file
               
                StreamWriter file = new StreamWriter(p);

                try
                {
                    foreach (string line in Messages)
                    {
                        file.WriteLine(line);
                    }
                }

                catch (Exception e) 
                {
                    if (ErrorMessages) BFS.Dialog.ShowMessageError("StoreLog, stream:\n" + e.ToString());
                }

                finally
                {
                    if (!(file == null)) file.Close();                    // Close the stream
                }       
            }
        }
    }
}


// The following code is adapted from this solution:
// https://stackoverflow.com/questions/36886355/retrieve-the-full-path-of-an-explorer-window-through-a-handle-in-c-sharp
// It builds a directory of all open Explorer windows
public class ExplorerWindowsHandler
{
    static Guid CLSID_ShellApplication = new Guid("13709620-C279-11CE-A49E-444553540000");
    Type shellApplicationType = Type.GetTypeFromCLSID(CLSID_ShellApplication, true);
    object shellApplication = null;
    object windows = null;
    Type windowsType = null;
    object count = null;
    Dictionary<IntPtr, string> ExpWndws = null;    // list of explorer windows
    Dictionary<IntPtr, int> ExpWndwsOrder = null;  // list of z-Order of the explorer windows
    IntPtr ThisScriptHandle = IntPtr.Zero;
    public bool ErrorMessages = false;
    public LogFile LogToFile = new LogFile();
    
    public void InitExplorerHandler(IntPtr thisscripthandle)
    {
        try
        {
            LogToFile.AddMessage("InitExplorerHandler: " + thisscripthandle.ToString(), true);
            
            shellApplication = Activator.CreateInstance(shellApplicationType);
            windows = shellApplicationType.InvokeMember("Windows", System.Reflection.BindingFlags.InvokeMethod, null, shellApplication, new object[] { });        
            windowsType = windows.GetType();
            count = windowsType.InvokeMember("Count", System.Reflection.BindingFlags.GetProperty, null, windows, null);
            ExpWndws = new Dictionary<IntPtr, string>();
            ExpWndwsOrder = new Dictionary<IntPtr, int>();
            ThisScriptHandle = thisscripthandle;
                        
            LogToFile.AddMessage(windowsType.ToString() + ": " + ((int)count).ToString());
            LogToFile.AddMessage(ExpWndws.ToString());
            LogToFile.AddMessage(ExpWndwsOrder.ToString());
            
            for (int i = 0; i < (int)count; i++)
            {
                object item = null;
                try
                {
                    item = windowsType.InvokeMember("Item", System.Reflection.BindingFlags.InvokeMethod, null, windows, new object[] { i });
                    if (item != null)    // Bugfix V1.1
                    {
                        Type itemType = item.GetType();
                        LogToFile.AddMessage(i.ToString() + ": " + itemType.ToString(), true);
                        string itemName = (string)itemType.InvokeMember("Name", System.Reflection.BindingFlags.GetProperty, null, item, null);
                        LogToFile.AddMessage(itemName);
                        if (itemName == "Windows Explorer" || itemName == "File Explorer" || itemName == "Explorer")
                        {
                            string itemHandle = itemType.InvokeMember("HWND", System.Reflection.BindingFlags.GetProperty, null, item, null).ToString();
                            // Cannot cast the member "HWND" to IntPtr, therefore need to find a different way get the handle of the window
                            LogToFile.AddMessage("itemHandle: " + itemHandle);
                            foreach (IntPtr handle in BFS.Window.GetAllWindowHandles())
                            {
                                if (handle.ToString() == itemHandle)
                                {
                                    if(!ExpWndws.ContainsKey(handle))
                                    {
                                        string itemPath = (string)itemType.InvokeMember("LocationURL", System.Reflection.BindingFlags.GetProperty, null, item, null);
                                        itemPath = itemPath.Replace(@"file:///", "");
                                        ExpWndws.Add(handle, itemPath);
                                        int z = GetWindowZOrder(handle);
                                        while (ExpWndwsOrder.ContainsValue(z)) {z++;} // Just to be sure that no double entry occurs
                                        ExpWndwsOrder.Add(handle, z);
                                        LogToFile.AddMessage("Add: " + handle.ToString() + ", " + z.ToString() + ", " + itemPath);
                                    }
                                    break;
                                }
                            }
                        }
                    }
                }
                catch (Exception e)
                {
                    if (ErrorMessages) BFS.Dialog.ShowMessageError("InitExplorerHandler Loop:\n" + e.ToString());
                    LogToFile.AddMessage("Exception in InitExplorerHandler Loop: ");
                    LogToFile.AddMessage(e.ToString());
                }
                finally
                {
                    LogToFile.AddMessage("Loop End", false);
                }
            }
        }
        catch (Exception e)
        {
            if (ErrorMessages) BFS.Dialog.ShowMessageError("InitExplorerHandler:\n" + e.ToString());
            LogToFile.AddMessage("Exception in InitExplorerHandler: ");
            LogToFile.AddMessage(e.ToString());
            count = 0;
        }
        finally
        {
            LogToFile.AddMessage("InitExplorerHandler End", false);
        }
    }
    
    // From: https://stackoverflow.com/a/61243105/3506904 
    [DllImport("user32.dll", SetLastError = true)]
    static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
    static int GetWindowZOrder(IntPtr hWnd)
    {
        var zOrder = -1;
        while ((hWnd = GetWindow(hWnd, 2 /* GW_HWNDNEXT */)) != IntPtr.Zero) zOrder++;
        return zOrder;
    }
    
    public string GetPath(IntPtr windowHandle, bool decode)
    {
        string returnPath = "";
        
        if(ExpWndws.ContainsKey(windowHandle))
        {
            returnPath = ExpWndws[windowHandle];
            if(decode)
            {
                returnPath = HttpUtility.UrlDecode(returnPath, System.Text.Encoding.Default);
            }
        }

        return returnPath;
    }
    
    public int GetOrder(IntPtr windowHandle)
    {
        int returnOrder = -1;
        
        if(ExpWndwsOrder.ContainsKey(windowHandle))
        {
            returnOrder = ExpWndwsOrder[windowHandle];
        }

        return returnOrder;
    }
    
    public IntPtr GetHandle(string path, bool close_duplicates)
    {
        IntPtr ret = IntPtr.Zero;
        foreach (KeyValuePair<IntPtr, string> window in ExpWndws) 
        {
            if(window.Value == path)
            {
                if (ret != IntPtr.Zero)
                {
                    // Close the duplicate window. If it is the window which is the handle for this 
                    // scripted function, close the other one.
                    IntPtr forClosure = window.Key;
                    if(forClosure == ThisScriptHandle)
                    {
                        forClosure = ret;
                        ret = window.Key;
                    }
                    try
                    {
                        BFS.Window.Close(forClosure);
                    }
                    catch {}
                }
                else
                {
                    ret = window.Key;
                }
                if(!close_duplicates) break;
            }
        }
        return ret;
    }
}