eltos / PasteIntoFile

Paste clipboard data into files and copy file contents directly via hotkey or context menu
MIT License
87 stars 6 forks source link

Pasting in autosave mode sometimes opens duplicate file explorer instance #9

Closed eltos closed 2 years ago

eltos commented 2 years ago

To Reproduce

  1. Open a special folder like "Downloads" in Windows file explorer (make sure it's ::{374DE290-123F-4565-9164-39C4925E467B} and not C:\Users\User\Downloads)
  2. With autosave enabled, right click > paste into file
  3. A new file explorer window (C:\Users\User\Downloads) will open with the new file selected for rename.

Expected behaviour
The file is selected for rename in the existing file explorer. Furthermore, when multiple windows with the same location are open, the one where the right-click action was performed is used.

Cause
This is caused by the use of SHOpenFolderAndSelectItems which does not re-use an existing file explorer instance where the path is a GUID (like ::{374DE290-123F-4565-9164-39C4925E467B} instead of C:\Users\User\Downloads) and also does not know which instance to re-use if there are multiple.


Related:

eltos commented 2 years ago

An alternative approach would be to manually iterate the open instances and find a matching one.

This works very well, except that it takes a moment until the newly created file appears in the Folder.Items() list. Therefore, if calling ExplorerUtil.RequestFilenameEdit(file) too early, it can not select the newly created file and the method falls back to SHOpenFolderAndSelectItems.
One could overcome this by introducing an artificial delay. Since SHOpenFolderAndSelectItems also takes a moment to select the file, this is no general disadvantage. However, the question remains how long the delay has to be, and if there is a way to use a callback or something in order not to introduce a hardcoded Thread.Sleep(1000);.

In ExplorerUtils.cs:

        /// <summary>
        /// Searches the file with given path in the given shell window and selects it if found
        /// </summary>
        /// <param name="window">The shell window</param>
        /// <param name="path">The path of the file to select</param>
        /// <param name="edit">Select in edit mode if true, otherwise just select</param>
        /// <returns>True if the file was found and selected, false otherwise</returns>
        private static bool SelectPathInWindow(SHDocVw.InternetExplorer window, string path, bool edit=true) {
            IShellFolderViewDual view = window?.Document as IShellFolderViewDual;
            if (view != null) {
                // TODO: some time needed for the file to appears in view.Folder.Items()
                foreach (FolderItem folderItem in view.Folder.Items()) {
                    if (folderItem.Path == path){
                        SetForegroundWindow((IntPtr)window.HWND);
                        // https://docs.microsoft.com/en-us/windows/win32/shell/shellfolderview-selectitem
                        view.SelectItem(folderItem, 16 /* focus it, */ + 8 /* ensure it's visible, */ 
                                                                   + 4 /* deselect all other and */ 
                                                                   + (edit ? 3 : 1) /* select or edit */);
                        return true;
                    }
                }
            }
            return false;
        }

        /// <summary>
        /// Request file name edit by user in active explorer path
        /// </summary>
        /// <param name="filePath">Path of file to select/edit</param>
        /// <param name="edit">can be set to false to select only (without entering edit mode)</param>
        public static void RequestFilenameEdit(string filePath, bool edit = true) {

            filePath = Path.GetFullPath(filePath);
            var dirPath = Path.GetDirectoryName(filePath);

            // check current shell window first
            var focussedWindow = GetActiveExplorer();
            if (GetExplorerPath(focussedWindow) == dirPath) {
                if (SelectPathInWindow(focussedWindow, filePath, edit))
                    return;
            }

            // then check other open shell windows
            var shellWindows = new SHDocVw.ShellWindows();
            foreach (SHDocVw.InternetExplorer window in shellWindows) {
                if (GetExplorerPath(window) == dirPath) {
                    if (SelectPathInWindow(window, filePath, edit))
                        return;
                }
            }

            // or open a new shell window
            IntPtr file;
            SHParseDisplayName(filePath, IntPtr.Zero, out file, 0, out _);
            try {
                SHOpenFolderAndSelectItems(file, 0, null, edit ? 1 : 0);
            } finally {
                ILFree(file);
            }
        }

        [DllImport("user32.dll")]
        static extern bool SetForegroundWindow(IntPtr hWnd);

        [DllImport("shell32.dll", SetLastError = true)]
        public static extern void SHParseDisplayName([MarshalAs(UnmanagedType.LPWStr)] string name,
            IntPtr bindingContext, [Out] out IntPtr pidl, uint sfgaoIn, [Out] out uint psfgaoOut);      
eltos commented 1 year ago

However, the question remains how long the delay has to be, and if there is a way to use a callback or something in order not to introduce a hardcoded Thread.Sleep(1000);.

This was solved with window.Refresh() and window.DocumentComplete. See ddfb8a4d907f2bcc63283a197321ccec3c58ebff.