diff --git a/Help.md b/Help.md index 6307225..e7d93ad 100644 --- a/Help.md +++ b/Help.md @@ -6,8 +6,9 @@ | --- | --- | | -gui=0 | Do not display the PersistentWindows icon on the System Tray. Effectively runs PersistentWindows as a service | -splash=0 | No splash window at PersistentWindows startup - | -legacy_icon | Switch to the original icon (as of 5.49 release) + | -legacy_icon | Switch to the original icon ![pwIcon2small](https://github.com/user-attachments/assets/4827f67a-2ce1-4a83-86da-b4bfa6835026) | -silent | No splash window, no balloon tip hint, no event logging + | -capture_floating_window=0 | Disable capture floating child window and dialog window position | -ignore_process "notepad.exe;foo" | Avoid restoring windows for the processes notepad.exe and foo | -debug_process "notepad.exe;foo" | Print the window positioning event logs in Event Viewer for the processes *notepad.exe* and *foo* | -foreground_background_dual_position=0 | Turn off dual position switching @@ -15,6 +16,7 @@ | -hotkey "Q" | register Alt + Q as the hotkey to (de)activate webpage commander window, default hotkey is "W" (Alt + W) | -ctrl_minimize_to_tray=0 | Turn off ctrl minimize window to notification tray | -prompt_session_restore | Ask the user before restoring the window layout upon resuming the last session. This may help reduce the total restore time for remote desktop sessions on slow internet connections. + | -delay_restart 5 | Restart PersistentWindows in 5 seconds. This option should only be used in case normal start of PersistentWindows fails. | -delay_auto_capture 1.0 | Adjust the lag between window move event and auto-capture to 1.0 second, the default lag is 3~4 seconds. | *-delay_auto_restore 2.5* | Adjust the lag between monitor on/off event and auto-restore to 2.5 seconds (the default lag is 1 second). This is in case the restore is incomplete or the monitor fails to go to sleep due to the restore starting too early. | -redraw_desktop | Redraw the whole desktop after a restore, in case some window workarea is not refreshed @@ -22,13 +24,17 @@ | -fix_offscreen_window=0 | Turn off auto correction of off-screen windows | -fix_unminimized_window=0 | Turn off auto restore of unminimized windows. Use this switch to avoid undesirable window shifting during window activation, which comes with Event id 9999 : "restore minimized window ...." in event viewer. |-auto_restore_new_display_session_from_db=0| Disable window restore from DB upon PC startup or switching display for the first time + |-auto_restore_existing_window_to_last_capture=1 | Turn on auto restore existing window from last capture upon PW start + |-auto_restore_new_window_to_last_capture=0 | Turn off auto restore new window to last killed position + |-pos_match_threshold 80 | Auto correct new window position to last killed position within range of 80 pixels, default is 40 | ‑auto_restore_missing_windows=1 | When restoring from disk, restore missing windows without prompting the user | ‑auto_restore_missing_windows=2 | At startup, automatically restore missing windows from disk. The user will be prompted before restoring each missing window | ‑auto_restore_missing_windows=3 | At startup, automatically restore missing windows from disk without prompting the user | -invoke_multi_window_process_only_once=0 | Launch an application multiple times when multiple windows of the same process need to be restored from the database. | -check_upgrade=0 | Disable the PersistentWindows upgrade check | -auto_upgrade=1 | Upgrade PersistentWindows automatically without user interaction - + | -dump_window_position_history=0 | Disable window position history dump + | -restore_snapshot "0" | restore snapshot 0 and exit. The range of snapshot id is [0-9a-z], as well as "~" or "`", the last two special ids represent the last auto restore. Note the window z-order can not be fully retored using this method, due to lack of capability to do multi-pass restore. --- ### Shortcuts to capture/restore snapshots @@ -54,7 +60,6 @@ * Dual Position Switching functionality: * Click the desktop window to bring the foreground window to its previous background position and Z-order. - * Shift + Click the desktop window to bring the foreground window to its *second* last background position. This is useful if the previous background position is mis-captured due to invoking start menu or other popups. * Ctrl + Click the desktop window to bring the foreground window to its previous Z-order while keeping the current location and size. * To cancel Dual Position Switching: @@ -110,7 +115,8 @@ | Alt + click commander window || send the mouse click to the underlying browser window ### Other features -* To replace the default app icon with your customized one: - * Rename your .ico (or .png) file as `pwIcon.ico` (or `pwIcon.png`) and copy it to the PersistentWindows program folder, or alternatively to `C:/Users//AppData/Local/PersistentWindows/`. - * Copy another icon file to the same directory and rename it to `pwIconBusy.*`. This icon is displayed when PersistentWindows is busy restoring windows. +* To replace the default app icons with customized one: + * Rename customized .ico (or .png) file as `pwIcon.ico` (or `pwIcon.png`) and copy it to the PersistentWindows program folder, or alternatively to `C:/Users//AppData/Local/PersistentWindows/`. + * Copy another ico/png file to the same directory and rename it to `pwIconBusy.*`. This icon is displayed when PersistentWindows is busy restoring windows. + * Copy yet another ico/png file to the same directory and rename it to `pwIconUpdate.*`. This icon is displayed when a new PersistentWindows release is available. diff --git a/Ninjacrab.PersistentWindows.Solution/Common/Common.csproj b/Ninjacrab.PersistentWindows.Solution/Common/Common.csproj index 7d1ee22..daecbb4 100644 --- a/Ninjacrab.PersistentWindows.Solution/Common/Common.csproj +++ b/Ninjacrab.PersistentWindows.Solution/Common/Common.csproj @@ -36,6 +36,7 @@ + @@ -93,21 +94,9 @@ - - DbKeySelect.cs - - - HotKeyWindow.cs - - - LayoutProfile.cs - LaunchProcess.cs - - NameDbKey.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/Ninjacrab.PersistentWindows.Solution/Common/HotKeyWindow.cs b/Ninjacrab.PersistentWindows.Solution/Common/HotKeyWindow.cs index b42d7ce..f4fd28a 100644 --- a/Ninjacrab.PersistentWindows.Solution/Common/HotKeyWindow.cs +++ b/Ninjacrab.PersistentWindows.Solution/Common/HotKeyWindow.cs @@ -6,7 +6,6 @@ using System.Data; using System.Drawing; using System.Linq; using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; using System.Runtime.InteropServices; @@ -21,25 +20,30 @@ namespace PersistentWindows.Common private uint hotkey; public static IntPtr commanderWnd = IntPtr.Zero; + public static bool invokedFromBrowser = false; private static System.Timers.Timer aliveTimer; + private static int callerAliveTimer = -1; //for tracing the starting source of alive timer + private System.Timers.Timer mouseScrollDelayTimer; private bool init = true; private bool active = false; private static bool tiny = false; private static bool browserWindowActivated = false; + private static bool restoring = false; private int origWidth; private int origHeight; - private int mouseOffset = 0; + private int mouseOffset = 0; //mouse dithering to workaround mouse location mis-update issue in rdp session private static POINT lastCursorPos; private POINT lastWheelCursorPos; private bool handCursor = false; + private bool ibeamCursor = false; private int titleHeight; - private static int callerAliveTimer = -1; //for tracing the starting source of alive timer private Color dfltBackColor; private bool promptZkey = true; private bool clickThrough = false; private bool defocused = false; + private int totalWaitSecondsForWhiteColor = 0; public HotKeyWindow(uint hkey) { @@ -203,6 +207,7 @@ namespace PersistentWindows.Common Visible = false; IntPtr fgwnd = GetForegroundWindow(); User32.SetForegroundWindow(fgwnd); + FgSleep(); if (alt_key_pressed || clickThrough) { @@ -273,7 +278,7 @@ namespace PersistentWindows.Common StartAliveTimer(1); } - bool IsBrowserWindow(IntPtr hwnd) + private bool IsBrowserWindow(IntPtr hwnd) { return PersistentWindowProcessor.IsBrowserWindow(hwnd); } @@ -591,9 +596,16 @@ namespace PersistentWindows.Common { if (!from_menu) { - IntPtr fgwnd = GetForegroundWindow(); - if (!IsBrowserWindow(fgwnd)) + IntPtr fgwnd = GetForegroundWindow(strict : true); + if (fgwnd == commanderWnd) + fgwnd = GetForegroundWindow(); + if (IsBrowserWindow(fgwnd)) { + invokedFromBrowser = true; + } + else + { + invokedFromBrowser = false; //forward hotkey char c = Convert.ToChar(hotkey); string cmd = $"%{c}"; @@ -629,13 +641,18 @@ namespace PersistentWindows.Common active = false; } } - } - public static void BrowserActivate(IntPtr hwnd, bool is_browser_window = true) + public static void BrowserActivate(IntPtr hwnd, bool is_browser_window = true, bool in_restore = false) { + if (browserWindowActivated == is_browser_window) + return; + browserWindowActivated = is_browser_window; + restoring = in_restore; + + Console.WriteLine($"browser activated {hwnd.ToString("X")}"); if (!tiny && !User32.IsWindowVisible(commanderWnd)) return; @@ -715,7 +732,7 @@ namespace PersistentWindows.Common return result; } - private bool IsSimilarColor(IntPtr hwnd, int x, int y, int xsize, int ysize) + private bool IsSimilarColor(IntPtr hwnd, int x, int y, int xsize, int ysize, Color px) { using (Bitmap screenPixel = new Bitmap(xsize, ysize)) { @@ -731,20 +748,16 @@ namespace PersistentWindows.Common } } - var p1 = screenPixel.GetPixel(0, 0); - var p2 = screenPixel.GetPixel(xsize - 1, 0); - Console.WriteLine($"pixel ({x}, {y}) {p1}"); - for (int i = 1; i < xsize; ++i) + Console.WriteLine($"pixel ({x}, {y}) {px}"); + for (int i = 0; i < xsize; ++i) { Color p = screenPixel.GetPixel(i, i); - if (DiffColor(p, p1) > 10) + if (DiffColor(p, px) > 15) return false; - p1 = p; p = screenPixel.GetPixel(xsize - i - 1, i); - if (DiffColor(p, p2) > 10) + if (DiffColor(p, px) > 15) return false; - p2 = p; } return true; @@ -783,6 +796,37 @@ namespace PersistentWindows.Common } } + private bool IsSameColor(IntPtr hwnd, int x, int y, int xsize, int ysize, Color px) + { + using (Bitmap screenPixel = new Bitmap(xsize, ysize)) + { + using (Graphics gdest = Graphics.FromImage(screenPixel)) + { + using (Graphics gsrc = Graphics.FromHwnd(hwnd)) + { + IntPtr hsrcdc = gsrc.GetHdc(); + IntPtr hdc = gdest.GetHdc(); + Gdi32.BitBlt(hdc, 0, 0, xsize, ysize, hsrcdc, x, y, (int)CopyPixelOperation.SourceCopy); + gdest.ReleaseHdc(); + gsrc.ReleaseHdc(); + } + } + + Console.WriteLine($"pixel ({x}, {y}) {px}"); + for (int i = 0; i < xsize; ++i) + { + var p = screenPixel.GetPixel(i, i); + if (p.ToArgb() != px.ToArgb()) + return false; + p = screenPixel.GetPixel(xsize - i - 1, i); + if (p.ToArgb() != px.ToArgb()) + return false; + } + + return true; + } + } + private void AliveTimerCallBack(Object source, ElapsedEventArgs e) { if (!active) @@ -793,12 +837,15 @@ namespace PersistentWindows.Common IntPtr fgwnd = GetForegroundWindow(); if (!PersistentWindowProcessor.IsBrowserWindow(fgwnd)) { - if (browserWindowActivated) - { + if (browserWindowActivated || restoring) StartAliveTimer(6, 1000); - return; - } - Visible = false; + else + Visible = false; + return; + } + else if (restoring) + { + StartAliveTimer(6, 1000); return; } @@ -823,7 +870,9 @@ namespace PersistentWindows.Common } else if (Math.Abs(cursorPos.X - lastCursorPos.X) > 3 || Math.Abs(cursorPos.Y - lastCursorPos.Y) > 3) { + ibeamCursor = false; //mouse moving, continue monitor + totalWaitSecondsForWhiteColor = 0; } else { @@ -831,39 +880,53 @@ namespace PersistentWindows.Common if (hCursor == Cursors.Default.Handle) { handCursor = false; - if (!commanderWndUnderCursor && !IsSimilarColor(IntPtr.Zero, cursorPos.X - Width/2, cursorPos.Y - Height/2, 12, 12)) + + if (callerAliveTimer == 6) { - // hide hotkey window to allow click through possible link - Visible = false; - clickThrough = true; + //browser activation + ; + } + else if (!commanderWndUnderCursor && !IsUniColor(IntPtr.Zero, cursorPos.X - Width / 2, cursorPos.Y - Height / 2, 12, 12)) + { + // shift commander window to allow click possible link + Left = cursorPos.X - 10; + Top = cursorPos.Y - 10; + totalWaitSecondsForWhiteColor = 0; StartAliveTimer(11, 1000); return; } - - if (!commanderWndUnderCursor && !IsUniColor(IntPtr.Zero, cursorPos.X - Width / 2, cursorPos.Y - Height / 2, 12, 12)) + else if (!commanderWndUnderCursor && !IsSimilarColor(IntPtr.Zero, cursorPos.X - Width / 2, cursorPos.Y - Height / 2, 1, 1, Color.White)) { - Left = cursorPos.X - 10; - Top = cursorPos.Y - 10; - StartAliveTimer(11, 1000); - return; + // wait for possible menu selection within webpage + ++totalWaitSecondsForWhiteColor; + if (Visible && totalWaitSecondsForWhiteColor < 3) + { + StartAliveTimer(11, 1000); + return; + } } } else if (hCursor == Cursors.IBeam.Handle) { + ibeamCursor = true; Visible = false; StartAliveTimer(11, 1000); return; } else if (hCursor == Cursors.Cross.Handle || handCursor) { + ibeamCursor = false; StartAliveTimer(7); return; } + totalWaitSecondsForWhiteColor = 0; + bool regain_focus = true; + bool change_to_visible = false; if (!Visible) { - Visible = true; + change_to_visible = true; TopMost = true; if (defocused) { @@ -871,7 +934,7 @@ namespace PersistentWindows.Common regain_focus = false; } } - else if (!handCursor) + else if (hCursor == Cursors.Default.Handle) { /* if (!commanderWndUnderCursor) @@ -882,15 +945,29 @@ namespace PersistentWindows.Common */ regain_focus = !commanderWndUnderCursor; } + else if (!handCursor) + { + regain_focus = false; + } + + if (ibeamCursor && hCursor == Cursors.Default.Handle) + //&& Math.Abs(cursorPos.X - lastCursorPos.X) < 3 && Math.Abs(cursorPos.Y - lastCursorPos.Y) < 3) + { + ibeamCursor = false; + StartAliveTimer(11, 1000); + return; + } // let tiny hotkey window follow cursor position ResetHotKeyVirtualDesktop(); + ResetHotkeyWindowPos(); + if (change_to_visible) + Visible = true; if (regain_focus) { User32.SetForegroundWindow(Handle); User32.SetFocus(Handle); } - ResetHotkeyWindowPos(); if (hCursor == Cursors.Default.Handle) { @@ -903,6 +980,7 @@ namespace PersistentWindows.Common // hand cursor shape if (!handCursor) { + ibeamCursor = false; handCursor = true; Left -= 10; } @@ -1054,9 +1132,9 @@ namespace PersistentWindows.Common User32.SetForegroundWindow(Handle); } - private static IntPtr GetForegroundWindow() + public static IntPtr GetForegroundWindow(bool strict = false) { - return PersistentWindowProcessor.GetForegroundWindow(); + return PersistentWindowProcessor.GetForegroundWindow(strict); } private void FormSizeChanged(object sender, EventArgs e) diff --git a/Ninjacrab.PersistentWindows.Solution/Common/HotKeyWindow.resx b/Ninjacrab.PersistentWindows.Solution/Common/HotKeyWindow.resx deleted file mode 100644 index 1af7de1..0000000 --- a/Ninjacrab.PersistentWindows.Solution/Common/HotKeyWindow.resx +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/Ninjacrab.PersistentWindows.Solution/Common/LayoutProfile.resx b/Ninjacrab.PersistentWindows.Solution/Common/LayoutProfile.resx deleted file mode 100644 index 1af7de1..0000000 --- a/Ninjacrab.PersistentWindows.Solution/Common/LayoutProfile.resx +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/Ninjacrab.PersistentWindows.Solution/Common/Models/ApplicationDisplayMetrics.cs b/Ninjacrab.PersistentWindows.Solution/Common/Models/ApplicationDisplayMetrics.cs index c24f53e..1c4acc4 100644 --- a/Ninjacrab.PersistentWindows.Solution/Common/Models/ApplicationDisplayMetrics.cs +++ b/Ninjacrab.PersistentWindows.Solution/Common/Models/ApplicationDisplayMetrics.cs @@ -1,7 +1,10 @@ using System; +using System.Text; +using System.Xml; +using System.Runtime.Serialization; + using PersistentWindows.Common.WinApiBridge; -using PersistentWindows.Common.Diagnostics; namespace PersistentWindows.Common.Models { @@ -23,6 +26,8 @@ namespace PersistentWindows.Common.Models public bool IsFullScreen { get; set; } public bool IsMinimized { get; set; } public bool IsInvisible { get; set; } + public long Style { get; set; } + public long ExtStyle { get; set; } // for restore window position to display session end time public DateTime CaptureTime { get; set; } @@ -55,7 +60,15 @@ namespace PersistentWindows.Common.Models public override string ToString() { //return string.Format("{0}.{1} {2}", ProcessId, HWnd.ToString("X8"), ProcessName); - return string.Format("{0}.{1:x8} {2}", ProcessId, HWnd.ToInt64(), ProcessName); + //return string.Format("process:{0:x4} hwnd:{1:x6} {2}", ProcessId, HWnd.ToInt64(), ProcessName); + DataContractSerializer dcs = new DataContractSerializer(typeof(ApplicationDisplayMetrics)); + StringBuilder sb = new StringBuilder(); + using (XmlWriter xw = XmlWriter.Create(sb)) + { + dcs.WriteObject(xw, this); + } + string xml = sb.ToString(); + return xml; } } } diff --git a/Ninjacrab.PersistentWindows.Solution/Common/NameDbKey.resx b/Ninjacrab.PersistentWindows.Solution/Common/NameDbKey.resx deleted file mode 100644 index 1af7de1..0000000 --- a/Ninjacrab.PersistentWindows.Solution/Common/NameDbKey.resx +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/Ninjacrab.PersistentWindows.Solution/Common/PersistentWindowProcessor.cs b/Ninjacrab.PersistentWindows.Solution/Common/PersistentWindowProcessor.cs index 8794cb3..fbafe35 100644 --- a/Ninjacrab.PersistentWindows.Solution/Common/PersistentWindowProcessor.cs +++ b/Ninjacrab.PersistentWindows.Solution/Common/PersistentWindowProcessor.cs @@ -9,6 +9,8 @@ using System.Threading; using System.Diagnostics; using System.Reflection; using Microsoft.Win32; +using System.Xml; +using System.Runtime.Serialization; using LiteDB; @@ -32,37 +34,40 @@ namespace PersistentWindows.Common public int UserForcedRestoreLatency = 0; private const int CaptureLatency = 3000; // delay in milliseconds from window OS move to capture private const int UserMoveLatency = 1000; // delay in milliseconds from user move/minimize/unminimize/maximize to capture, must < CaptureLatency + private const int ForegroundTimerLatency = UserMoveLatency / 4; private const int MaxUserMoves = 4; // max user window moves per capture cycle private const int MinWindowOsMoveEvents = 12; // threshold of window move events initiated by OS per capture cycle - private const int MaxSnapshots = 38; // 0-9, a-z, ` and final one for undo - private const int MaxHistoryQueueLength = 40; // must be bigger than MaxSnapshots + 1 + private const int MaxSnapshots = 38; // 0-9, a-z, ` (for redo last auto restore) and final one for undo last snapshot restore + //[MaxSnapshots] is current capture, [MaxSnapshots + 1] is previous capture + private const int MaxHistoryQueueLength = 41; // ideally bigger than MaxSnapshots + 2 private const int PauseRestoreTaskbar = 3500; //cursor idle time before dragging taskbar + private const int MinClassNamePrefix = 8; //allow partial class name matching during inheritance + public int MaxDiffPos = 40; //allow matching window of slightly different position private bool initialized = false; // window position database private Dictionary>> monitorApplications = new Dictionary>>(); //in-memory database of live windows - private Dictionary>> deadApps - = new Dictionary>>(); //database of killed windows - private Int64 lastKilledWindowId = 0; //monotonically increasing unique id for every killed window + private Dictionary>> deadApps + = new Dictionary>>(); //database of killed windows + //private long lastKilledWindowId = 0; //monotonically increasing unique id for every killed window private string persistDbName = null; //on-disk database name private Dictionary lastCursorPos = new Dictionary(); + public bool captureFloatingWindow = true; private HashSet allUserMoveWindows = new HashSet(); private HashSet unResponsiveWindows = new HashSet(); - private HashSet noRecordWindows = new HashSet(); private static IntPtr desktopWindow = User32.GetDesktopWindow(); private static IntPtr vacantDeskWindow = IntPtr.Zero; - private bool restoreHotkeyWindow = false; + private uint fakeHwnd = 1; //for resolving handle value conflict of live and dead window + public bool resolveHwndConflict = true; // windows that are not to be restored private static HashSet noRestoreWindows = new HashSet(); //windows excluded from auto-restore private static HashSet noRestoreWindowsTmp = new HashSet(); //user moved windows during restore // realtime fixing window location - private IntPtr curMovingWnd = IntPtr.Zero; - private Timer moveTimer; // when user move a window private Timer foregroundTimer; // when user bring a window to foreground private DateTime lastDisplayChangeTime = DateTime.Now; @@ -71,6 +76,7 @@ namespace PersistentWindows.Common // capture control private Timer captureTimer; + private bool captureTimerStarted = false; private string curDisplayKey; // current display config name public string dbDisplayKey = null; private static Dictionary windowTitle = new Dictionary(); // for matching running window with DB record @@ -87,22 +93,28 @@ namespace PersistentWindows.Common private IntPtr realForeGroundWindow = IntPtr.Zero; public Dictionary processCmd = new Dictionary(); private HashSet fullScreenGamingWindows = new HashSet(); + private HashSet fullScreenGamingProcesses = new HashSet(); + private HashSet fullScreenGamingConfig = new HashSet(); + private IntPtr fullScreenGamingWindow = IntPtr.Zero; private bool exitFullScreenGaming = false; private POINT initCursorPos; private bool freezeCapture = false; public bool rejectScaleFactorChange = true; + private Object captureLock = new object(); // restore control private Timer restoreTimer; private Timer restoreFinishedTimer; public bool restoringFromMem = false; // automatic restore from memory or snapshot + private bool restoreSingleWindow = false; public bool restoringFromDB = false; // manual restore from DB public bool autoInitialRestoreFromDB = false; public bool restoringSnapshot = false; // implies restoringFromMem + private Object restoringFullScreenWindow = new object(); public bool showDesktop = false; // show desktop when display changes public int fixZorder = 1; // 1 means restore z-order only for snapshot; 2 means restore z-order for all; 0 means no z-order restore at all public int fixZorderMethod = 5; // bit i represent restore method for pass i - public bool fixTaskBar = true; + public int fixTaskBar = 1; public bool pauseAutoRestore = false; public bool promptSessionRestore = false; public bool redrawDesktop = false; @@ -110,15 +122,18 @@ namespace PersistentWindows.Common public bool enhancedOffScreenFix = false; public bool fixUnminimizedWindow = true; public bool autoRestoreMissingWindows = false; - public bool autoRestoreLiveWindows = true; //for new display session, autorestore live windows using data from db (without resurrecting dead one) + public bool autoRestoreLiveWindowsFromDb = true; //for new display session, autorestore live windows using data from db (without resurrecting dead one) + public bool autoRestoreNewWindowToLastCapture = true; public bool launchOncePerProcessId = true; private int restoreTimes = 0; //multiple passes need to fully restore private Object restoreLock = new object(); private Object dbLock = new object(); private bool restoreHalted = false; public int haltRestore = 3000; //milliseconds to wait to finish current halted restore and restart next one + private const int immediateFinishRestore = 20; private HashSet restoredWindows = new HashSet(); private HashSet topmostWindowsFixed = new HashSet(); + public bool fastRestore = true; public bool enableDualPosSwitch = true; private HashSet dualPosSwitchWindows = new HashSet(); @@ -135,13 +150,18 @@ namespace PersistentWindows.Common "chrome", "firefox", "msedge", "vivaldi", "opera", "brave", "360ChromeX" }; + public bool dumpHistoryData = true; + private string windowPosDataFile = "window_pos.xml"; //for PW restart without PC reboot + private string snapshotTimeFile = "snapshot_time.xml"; + private string debugWindowDump = "debug_window.xml"; + private HashSet ignoreProcess = new HashSet(); private HashSet debugProcess = new HashSet(); private HashSet debugWindows = new HashSet(); private static Dictionary windowProcessName = new Dictionary(); private Process process; - private ProcessPriorityClass processPriority; + public ProcessPriorityClass processPriority; private string appDataFolder; public bool redirectAppDataFolder = false; @@ -152,8 +172,6 @@ namespace PersistentWindows.Common private bool remoteSession = false; // restore time - private Dictionary lastUserActionTime = new Dictionary(); - private Dictionary lastUserActionTimeBackup = new Dictionary(); private Dictionary> snapshotTakenTime = new Dictionary>(); public int snapshotId; @@ -177,6 +195,7 @@ namespace PersistentWindows.Common private EventHandler displaySettingsChangingHandler; private EventHandler displaySettingsChangedHandler; private SessionSwitchEventHandler sessionSwitchEventHandler; + private SessionEndingEventHandler sessionEndingEventHandler; private readonly List winEventHooks = new List(); private User32.WinEventDelegate winEventsCaptureDelegate; @@ -197,6 +216,172 @@ namespace PersistentWindows.Common ; } #endif + + private void DumpSnapshotTakenTime() + { + DataContractSerializer dcs2 = new DataContractSerializer(typeof(Dictionary>)); + StringBuilder sb2 = new StringBuilder(); + using (XmlWriter xw = XmlWriter.Create(sb2)) + { + dcs2.WriteObject(xw, snapshotTakenTime); + } + string xml2 = sb2.ToString(); + File.WriteAllText(Path.Combine(appDataFolder, snapshotTimeFile), xml2, Encoding.Unicode); + } + + private void TrimDumpHistory(Dictionary>> dump_apps) + { + foreach (var display_key in dump_apps.Keys) + { + foreach (var hwnd in dump_apps[display_key].Keys) + { + if (dualPosSwitchWindows.Contains(hwnd)) + continue; + + List invalid_entries = new List(); + for (int i = 0; i < dump_apps[display_key][hwnd].Count; ++i) + { + if (!dump_apps[display_key][hwnd][i].IsValid) + invalid_entries.Add(i); + } + for (int i = invalid_entries.Count - 1; i >= 0; --i) + { + dump_apps[display_key][hwnd].RemoveAt(invalid_entries[i]); + } + + invalid_entries.Clear(); + for (int i = 0; i < dump_apps[display_key][hwnd].Count; ++i) + { + if (dump_apps[display_key][hwnd][i].SnapShotFlags != 0) + continue; + invalid_entries.Add(i); + } + + //keep the last record + for (int i = invalid_entries.Count - 2; i >= 0; --i) + { + dump_apps[display_key][hwnd].RemoveAt(invalid_entries[i]); + } + } + } + } + + private void WriteDebugWindowHistory(Dictionary>> allApps) + { + if (debugWindows.Count > 0) + { + DataContractSerializer dcs = new DataContractSerializer(typeof(List)); + StringBuilder s = new StringBuilder(); + using (XmlWriter xw = XmlWriter.Create(s)) + { + foreach (var hwnd in debugWindows) + { + if (!allApps[curDisplayKey].ContainsKey(hwnd)) + continue; + dcs.WriteObject(xw, allApps[curDisplayKey][hwnd]); + break; + } + } + string x = s.ToString(); + File.WriteAllText(Path.Combine(appDataFolder, debugWindowDump), x, Encoding.Unicode); + } + } + + private void WriteDataDumpCore(bool dump_dead_window) + { + DataContractSerializer dcs = new DataContractSerializer(typeof(Dictionary>>)); + StringBuilder sb = new StringBuilder(); + using (XmlWriter xw = XmlWriter.Create(sb)) + { + if (dump_dead_window) + { + var allApps = new Dictionary>>(); //in-memory database of live windows + foreach (var display_key in monitorApplications.Keys) + { + allApps[display_key] = new Dictionary>(); + foreach (var hwnd in monitorApplications[display_key].Keys) + { + allApps[display_key][hwnd] = new List(monitorApplications[display_key][hwnd]); + } + } + + foreach (var display_key in deadApps.Keys) + { + if (!allApps.ContainsKey(display_key)) + continue; + foreach (var hwnd in deadApps[display_key].Keys) + { + if (allApps[display_key].ContainsKey(hwnd)) + continue; + allApps[display_key][hwnd] = new List(deadApps[display_key][hwnd]); + } + } + + TrimDumpHistory(allApps); + + dcs.WriteObject(xw, allApps); + + WriteDebugWindowHistory(allApps); + } + else + dcs.WriteObject(xw, monitorApplications); + } + string xml = sb.ToString(); + File.WriteAllText(Path.Combine(appDataFolder, windowPosDataFile), xml, Encoding.Unicode); + + DumpSnapshotTakenTime(); + } + public void WriteDataDump(bool dump_dead_window = true) + { + try + { + if (dumpHistoryData) + WriteDataDumpCore(dump_dead_window); + } + catch (Exception e) + { + Log.Error(e.ToString()); + } + } + + private void ReadDataDump() + { + string path = Path.Combine(appDataFolder, windowPosDataFile); + if (!File.Exists(path)) + return; + DataContractSerializer dcs = new DataContractSerializer(typeof(Dictionary>>)); + using (FileStream fs = File.OpenRead(path)) + using (XmlReader xr = XmlReader.Create(fs)) + { + deadApps = (Dictionary>>)dcs.ReadObject(xr); + } + //File.Delete(Path.Combine(appDataFolder, windowPosDataFile)); + + string path2 = Path.Combine(appDataFolder, snapshotTimeFile); + if (!File.Exists(path2)) + return; + DataContractSerializer dcs2 = new DataContractSerializer(typeof(Dictionary>)); + using (FileStream fs = File.OpenRead(path2)) + using (XmlReader xr = XmlReader.Create(fs)) + { + snapshotTakenTime = (Dictionary>)dcs2.ReadObject(xr); + } + //File.Delete(Path.Combine(appDataFolder, snapshotTimeFile)); + } + + private void ReadDataDumpSafe() + { + try + { + if (dumpHistoryData) + ReadDataDump(); + } + catch (Exception e) + { + Log.Error(e.ToString()); + } + } + private void CleanupDisplayRegKey(string key) { if (key.Contains("__")) @@ -205,7 +390,7 @@ namespace PersistentWindows.Common { CleanupDisplayRegKeyCore(key); } - catch(Exception ex) + catch (Exception ex) { Log.Error(ex.ToString()); } @@ -247,11 +432,172 @@ namespace PersistentWindows.Common top.DeleteSubKeyTree(config_name); } } - public bool Start(bool auto_restore_from_db = false) + + private bool SnapshotExists(string displayKey) + { + if (!snapshotTakenTime.ContainsKey(displayKey)) + return false; + + foreach (var id in snapshotTakenTime[displayKey].Keys) + { + // 26 + 10 maximum manual snapshots + if (id < 36) + return true; + } + + return false; + } + + private bool RestoreExists(string displayKey) + { + if (!snapshotTakenTime.ContainsKey(displayKey)) + return false; + + if (snapshotTakenTime[displayKey].Keys.Contains(MaxSnapshots + 1)) + { + return true; + } + + return false; + } + + private void foregroundTimerCallback(object state) + { + IntPtr hwnd = foreGroundWindow; + + // Occasionaly OS might bring a window to foreground upon sleep + // If the window move is initiated by OS (before sleep), + // keep restart capture timer would eventually discard these moves + // either by power suspend event handler calling CancelCaptureTimer() + // or due to capture timer handler found too many window moves + + // If the window move is caused by user snapping window to screen edge, + // delay capture by a few seconds should be fine. + + if (monitorApplications.ContainsKey(curDisplayKey) + && monitorApplications[curDisplayKey].ContainsKey(hwnd)) + { + //capture with slight delay inperceivable by user, required for full screen mode recovery + userMove = true; + StartCaptureTimer(UserMoveLatency / 2); + } + else if (fullScreenGamingWindow == IntPtr.Zero) + { + //create window event may be delayed + if (hwnd != IntPtr.Zero && !noRestoreWindows.Contains(hwnd)) + CaptureWindow(hwnd, 0, DateTime.Now, curDisplayKey); + + StartCaptureTimer(); + + //speed up initial capture + POINT cursorPos; + User32.GetCursorPos(out cursorPos); + if (!cursorPos.Equals(initCursorPos)) + userMove = true; + } + + if (!sessionActive) //disable foreground event handling + return; + if (!User32.IsWindow(hwnd)) + return; + + if (hwnd == fullScreenGamingWindow) + return; + + if (noRestoreWindows.Contains(hwnd)) + return; + + bool ctrl_key_pressed = (User32.GetKeyState(0x11) & 0x8000) != 0; + bool alt_key_pressed = (User32.GetKeyState(0x12) & 0x8000) != 0; + bool shift_key_pressed = (User32.GetKeyState(0x10) & 0x8000) != 0; + + int leftClicks = leftButtonClicks; + leftButtonClicks = 0; + + if (realForeGroundWindow == vacantDeskWindow) + { + if (leftClicks != 1) + return; + + if (!ctrl_key_pressed && !alt_key_pressed) + { + //restore window to previous background position + SwitchForeBackground(hwnd); + } + else if (ctrl_key_pressed && !alt_key_pressed) + { + //restore to previous background zorder with current size/pos + SwitchForeBackground(hwnd, strict_dps_check: false, updateBackgroundPos: true); + } + else if (!ctrl_key_pressed && alt_key_pressed && !shift_key_pressed) + { + FgWindowToBottom(); + } + } + else if (!ctrl_key_pressed && !shift_key_pressed) + { + if (fullScreenGamingWindows.Contains(hwnd)) + return; + + ActivateWindow(hwnd); //window could be active on alt-tab + if (IsFullScreen(hwnd) || IsRdpWindow(hwnd)) + { + if (User32.IsWindowVisible(HotKeyWindow.commanderWnd)) + { + RECT hkwinPos = new RECT(); + User32.GetWindowRect(HotKeyWindow.commanderWnd, ref hkwinPos); + + RECT fgwinPos = new RECT(); + User32.GetWindowRect(hwnd, ref fgwinPos); + + RECT intersect = new RECT(); + bool overlap = User32.IntersectRect(out intersect, ref hkwinPos, ref fgwinPos); + if (overlap) + { + User32.ShowWindow(HotKeyWindow.commanderWnd, (int)ShowWindowCommands.Hide); + } + } + } + + if (!alt_key_pressed) + { + /* + if (pendingMoveEvents.Count > 1) + return; + */ + + //restore window to previous foreground position + SwitchForeBackground(hwnd, toForeground: true); + } + } + + } + + public void RestoreSnapshotCmd(int id) + { + string productName = System.Windows.Forms.Application.ProductName; + appDataFolder = redirectAppDataFolder ? "." : + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), productName); +#if DEBUG + //avoid db path conflict with release version + //appDataFolder = "."; + appDataFolder = AppDomain.CurrentDomain.BaseDirectory; +#endif + + ReadDataDumpSafe(); + curDisplayKey = GetDisplayKey(); + CaptureNewDisplayConfig(curDisplayKey); + + //RestoreSnapshot(id); + restoringSnapshot = true; + snapshotId = id; + restoringFromMem = true; + RestoreApplicationsOnCurrentDisplays(curDisplayKey, IntPtr.Zero, DateTime.Now); + } + + public bool Start(bool auto_restore_from_db, bool auto_restore_last_capture_at_startup) { process = Process.GetCurrentProcess(); - processPriority = process.PriorityClass; - string productName = System.Windows.Forms.Application.ProductName; appDataFolder = redirectAppDataFolder ? "." : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), productName); @@ -261,7 +607,6 @@ namespace PersistentWindows.Common //appDataFolder = "."; appDataFolder = AppDomain.CurrentDomain.BaseDirectory; #endif - var dir = Directory.CreateDirectory(appDataFolder); try { @@ -287,7 +632,9 @@ namespace PersistentWindows.Common bool found_latest_db_file_version = false; if (File.Exists(persistDbName)) found_latest_db_file_version = true; - foreach (var file in dir.EnumerateFiles($@"{productName}*.db")) + + var dir_info = new DirectoryInfo(appDataFolder); + foreach (var file in dir_info.EnumerateFiles($@"{productName}*.db")) { var fname = file.Name; if (found_latest_db_file_version && !fname.Contains(db_version)) @@ -312,6 +659,7 @@ namespace PersistentWindows.Common } } + ReadDataDumpSafe(); curDisplayKey = GetDisplayKey(); CaptureNewDisplayConfig(curDisplayKey); @@ -325,110 +673,14 @@ namespace PersistentWindows.Common debugTimer.Change(2000, 2000); #endif - moveTimer = new Timer(state => - { - if ((User32.GetKeyState(0x11) & 0x8000) != 0 //ctrl key pressed - && (User32.GetKeyState(0x10) & 0x8000) != 0) //shift key pressed - { - Log.Event("ignore window {0}", GetWindowTitle(curMovingWnd)); - noRestoreWindows.Add(curMovingWnd); - } - } - ); - - foregroundTimer = new Timer(state => - { - if (!sessionActive) //disable foreground event handling - return; - - IntPtr hwnd = foreGroundWindow; - if (!User32.IsWindow(hwnd)) - return; - - if (noRestoreWindows.Contains(hwnd)) - return; - - bool ctrl_key_pressed = (User32.GetKeyState(0x11) & 0x8000) != 0; - bool alt_key_pressed = (User32.GetKeyState(0x12) & 0x8000) != 0; - bool shift_key_pressed = (User32.GetKeyState(0x10) & 0x8000) != 0; - - int leftClicks = leftButtonClicks; - leftButtonClicks = 0; - - if (realForeGroundWindow == vacantDeskWindow) - { - if (leftClicks != 1) - return; - - if (!ctrl_key_pressed && !alt_key_pressed) - { - //restore window to previous background position - SwitchForeBackground(hwnd, secondBackGround:shift_key_pressed); - } - else if (ctrl_key_pressed && !alt_key_pressed) - { - //restore to previous background zorder with current size/pos - SwitchForeBackground(hwnd, updateBackgroundPos: true, secondBackGround:shift_key_pressed); - } - else if (!ctrl_key_pressed && alt_key_pressed && !shift_key_pressed) - { - FgWindowToBottom(); - } - } - else if (!ctrl_key_pressed && !shift_key_pressed) - { - if (fullScreenGamingWindows.Contains(hwnd)) - return; - - ActivateWindow(hwnd); //window could be active on alt-tab - if (IsFullScreen(hwnd) || IsRdpWindow(hwnd)) - { - if (User32.IsWindowVisible(HotKeyWindow.commanderWnd)) - { - RECT hkwinPos = new RECT(); - User32.GetWindowRect(HotKeyWindow.commanderWnd, ref hkwinPos); - - RECT fgwinPos = new RECT(); - User32.GetWindowRect(hwnd, ref fgwinPos); - - RECT intersect = new RECT(); - bool overlap = User32.IntersectRect(out intersect, ref hkwinPos, ref fgwinPos); - if (overlap) - { - restoreHotkeyWindow = true; - User32.ShowWindow(HotKeyWindow.commanderWnd, (int)ShowWindowCommands.Hide); - } - } - } - else if (restoreHotkeyWindow) - { - restoreHotkeyWindow = false; - User32.ShowWindow(HotKeyWindow.commanderWnd, (int)ShowWindowCommands.Show); - User32.SetForegroundWindow(HotKeyWindow.commanderWnd); - User32.SetFocus(HotKeyWindow.commanderWnd); - } - - if (!alt_key_pressed) - { - /* - if (pendingMoveEvents.Count > 1) - return; - */ - - //restore window to previous foreground position - SwitchForeBackground(hwnd, toForeground: true); - } - } - - if (freezeCapture || !monitorApplications.ContainsKey(curDisplayKey) || !monitorApplications[curDisplayKey].ContainsKey(hwnd)) - return; - - if (normalSessions.Contains(curDisplayKey)) - CaptureApplicationsOnCurrentDisplays(curDisplayKey, immediateCapture:true); - }); + foregroundTimer = new Timer(foregroundTimerCallback); captureTimer = new Timer(state => { + process.PriorityClass = processPriority; + + captureTimerStarted = false; + userMovePrev = userMove; userMove = false; @@ -438,20 +690,24 @@ namespace PersistentWindows.Common if (restoringFromMem) return; + if (freezeCapture) + return; + /* + if (foreGroundWindow != IntPtr.Zero && fullScreenGamingWindow == foreGroundWindow) + { + fullScreenGamingWindow = IntPtr.Zero; + return; + } + if (fullScreenGamingWindows.Contains(foreGroundWindow)) return; - - foreach (var hwnd in fullScreenGamingWindows) - { - if (IsFullScreen(hwnd)) - return; - } + */ Log.Trace("Capture timer expired"); BatchCaptureApplicationsOnCurrentDisplays(); }); - restoreTimer = new Timer(state => { TimerRestore(); }); + restoreTimer = new Timer(TimerRestore); restoreFinishedTimer = new Timer(state => { @@ -459,7 +715,6 @@ namespace PersistentWindows.Common int restorePass = restoreTimes; unResponsiveWindows.Clear(); - noRecordWindows.Clear(); bool wasRestoringFromDB = restoringFromDB; restoringFromDB = false; @@ -467,7 +722,8 @@ namespace PersistentWindows.Common restoringFromMem = false; bool wasRestoringSnapshot = restoringSnapshot; restoringSnapshot = false; - exitFullScreenGaming = false; + if (fullScreenGamingWindow == IntPtr.Zero) + exitFullScreenGaming = false; ResetState(); Log.Trace(""); @@ -479,7 +735,7 @@ namespace PersistentWindows.Common restoreHalted = false; topmostWindowsFixed.Clear(); - Log.Error("Restore aborted for {0}", curDisplayKey); + Log.Error("Restore aborted for {0}", displayKey); curDisplayKey = displayKey; if (fullScreenGamingWindows.Contains(foreGroundWindow) || !normalSessions.Contains(curDisplayKey)) @@ -492,7 +748,6 @@ namespace PersistentWindows.Common //restore icon to idle hideRestoreTip(); iconBusy = false; - sessionActive = true; } else { @@ -520,14 +775,12 @@ namespace PersistentWindows.Common { if (!snapshotTakenTime.ContainsKey(curDisplayKey)) snapshotTakenTime[curDisplayKey] = new Dictionary(); - if (lastUserActionTime.ContainsKey(curDisplayKey)) - snapshotTakenTime[curDisplayKey][MaxSnapshots - 2] = lastUserActionTime[curDisplayKey]; + if (snapshotTakenTime[curDisplayKey].ContainsKey(MaxSnapshots)) + snapshotTakenTime[curDisplayKey][MaxSnapshots - 2] = snapshotTakenTime[curDisplayKey][MaxSnapshots]; } - if (wasRestoringSnapshot || noRestoreWindowsTmp.Count > 0) - CaptureApplicationsOnCurrentDisplays(curDisplayKey, immediateCapture: true); - else - StartCaptureTimer(); + CaptureApplicationsOnCurrentDisplays(curDisplayKey, immediateCapture: true); + freezeCapture = false; } bool db_exist = false; @@ -545,9 +798,8 @@ namespace PersistentWindows.Common } enableRestoreMenu(db_exist, checkUpgrade); - freezeCapture = false; - bool snapshot_exist = snapshotTakenTime.ContainsKey(curDisplayKey); + bool snapshot_exist = SnapshotExists(curDisplayKey); enableRestoreSnapshotMenu(snapshot_exist); //changeIconText(null); @@ -599,9 +851,9 @@ namespace PersistentWindows.Common 0, (uint)User32Events.WINEVENT_OUTOFCONTEXT)); - // capture window close + // capture window create/close this.winEventHooks.Add(User32.SetWinEventHook( - User32Events.EVENT_OBJECT_DESTROY, + User32Events.EVENT_OBJECT_CREATE, User32Events.EVENT_OBJECT_DESTROY, IntPtr.Zero, winEventsCaptureDelegate, @@ -609,14 +861,37 @@ namespace PersistentWindows.Common 0, (uint)User32Events.WINEVENT_OUTOFCONTEXT)); + this.sessionEndingEventHandler = + (s, e) => + { + process.PriorityClass = ProcessPriorityClass.High; + EndDisplaySession(); + WriteDataDump(); + Log.Event("Session ending"); + }; + SystemEvents.SessionEnding += sessionEndingEventHandler; + this.displaySettingsChangingHandler = (s, e) => { + if (fastRestore) + process.PriorityClass = ProcessPriorityClass.High; + if (!freezeCapture) { lastDisplayChangeTime = DateTime.Now; EndDisplaySession(); freezeCapture = true; + + if (normalSessions.Contains(curDisplayKey)) + { + // rewind disqualified capture time + string display_key = GetDisplayKey(); + if (!fullScreenGamingConfig.Contains(display_key)) + UndoCapture(lastDisplayChangeTime); + + Log.Event("Display session changed"); + } } }; SystemEvents.DisplaySettingsChanging += this.displaySettingsChangingHandler; @@ -627,22 +902,6 @@ namespace PersistentWindows.Common string displayKey = GetDisplayKey(); Log.Event("Display settings changed {0}", displayKey); - // undo disqualified capture time - if (lastUserActionTime.ContainsKey(curDisplayKey)) - { - var lastCaptureTime = lastUserActionTime[curDisplayKey]; - var diff = lastDisplayChangeTime - lastCaptureTime; - if (diff.TotalMilliseconds < CaptureLatency) - { - if (lastUserActionTimeBackup.ContainsKey(curDisplayKey)) - { - lastUserActionTime[curDisplayKey] = lastUserActionTimeBackup[curDisplayKey]; - Log.Error("undo capture of {0} at {1}", curDisplayKey, lastCaptureTime); - } - } - } - - { EndDisplaySession(); @@ -675,7 +934,7 @@ namespace PersistentWindows.Common { PromptSessionRestore(); } - if (autoRestoreLiveWindows && !monitorApplications.ContainsKey(displayKey)) + if (autoRestoreLiveWindowsFromDb && !monitorApplications.ContainsKey(displayKey)) { CaptureApplicationsOnCurrentDisplays(displayKey, immediateCapture: true); Log.Event("auto restore from db"); @@ -687,24 +946,27 @@ namespace PersistentWindows.Common else { restoringFromMem = true; - StartRestoreTimer(milliSecond: UserForcedRestoreLatency > 0 ? UserForcedRestoreLatency : RestoreLatency); + StartRestoreTimer(); } } else if (error == 0 && pquns.HasFlag(Shell32.QUERY_USER_NOTIFICATION_STATE.QUNS_RUNNING_D3D_FULL_SCREEN)) { + fullScreenGamingWindow = foreGroundWindow; + fullScreenGamingProcesses.Add(windowProcessName[fullScreenGamingWindow]); + fullScreenGamingConfig.Add(displayKey); if (IsNewWindow(foreGroundWindow)) { - fullScreenGamingWindows.Add(foreGroundWindow); + fullScreenGamingWindows.Add(fullScreenGamingWindow); Log.Event($"enter full-screen gaming mode {displayKey} {GetWindowTitle(foreGroundWindow)}"); } else Log.Event($"re-enter full-screen gaming mode"); - StartRestoreFinishedTimer(0); + StartRestoreFinishedTimer(immediateFinishRestore); } else { - StartRestoreFinishedTimer(0); + StartRestoreFinishedTimer(immediateFinishRestore); } } } @@ -755,6 +1017,7 @@ namespace PersistentWindows.Common case SessionSwitchReason.SessionLock: Log.Event("Session closing: reason {0}", args.Reason); { + UndoCapture(DateTime.Now); sessionLocked = true; sessionActive = false; EndDisplaySession(); @@ -792,9 +1055,11 @@ namespace PersistentWindows.Common }; SystemEvents.SessionSwitch += sessionSwitchEventHandler; - initialized = true; + remoteSession = System.Windows.Forms.SystemInformation.TerminalServerSession; + bool sshot_exist = SnapshotExists(curDisplayKey); + enableRestoreSnapshotMenu(sshot_exist); Log.Event($"Display config is {curDisplayKey}"); using (var persistDB = new LiteDatabase(persistDbName)) { @@ -806,13 +1071,22 @@ namespace PersistentWindows.Common { normalSessions.Add(item); } + + var ticks = Kernel32.GetTickCount64(); + if (ticks > 300000) //system up 5min + return true; + if (db_exist && auto_restore_from_db) { restoringFromDB = true; dbDisplayKey = curDisplayKey; StartRestoreTimer(); } - else if (db_exist && autoRestoreLiveWindows) + else if (auto_restore_last_capture_at_startup && RestoreExists(curDisplayKey)) + { + RestoreSnapshot(MaxSnapshots + 1); + } + else if (db_exist && autoRestoreLiveWindowsFromDb) { Log.Event("auto restore from db"); restoringFromDB = true; @@ -907,6 +1181,10 @@ namespace PersistentWindows.Common bool isFullScreen = false; if ((style & (long)WindowStyleFlags.MAXIMIZEBOX) == 0L) { + // mstsc in full-screen mode may report inaccurate size such as 3858 x 2207 on 4k monitor + if (windowProcessName.ContainsKey(hwnd) && windowProcessName[hwnd] == "mstsc") + return true; + RECT screenPosition = new RECT(); User32.GetWindowRect(hwnd, ref screenPosition); @@ -914,23 +1192,14 @@ namespace PersistentWindows.Common if (curDisplayKey.Contains(size)) isFullScreen = true; - if (!isFullScreen) + List displays = GetDisplays(); + foreach (var display in displays) { - List displays = GetDisplays(); - foreach (var display in displays) - { - RECT screen = display.Position; - RECT intersect = new RECT(); - if (!User32.IntersectRect(out intersect, ref screenPosition, ref screen)) - { - //must intersect with all screens - isFullScreen = false; - break; - } - - if (intersect.Equals(screen)) - isFullScreen = true; //fully covers at least one screen - } + RECT screen = display.Position; + RECT intersect = new RECT(); + if (User32.IntersectRect(out intersect, ref screenPosition, ref screen)) + if (intersect.Equals(screen)) //fully covers at least one screen + isFullScreen = true; } } @@ -942,15 +1211,22 @@ namespace PersistentWindows.Common if (use_cache && windowTitle.ContainsKey(hwnd)) return windowTitle[hwnd]; - var length = User32.GetWindowTextLength(hwnd); - if (length > 0) + try { - length++; - var title = new StringBuilder(length); - User32.GetWindowText(hwnd, title, length); - var t = title.ToString(); - t = t.Trim(); - return t; + var length = User32.GetWindowTextLength(hwnd); + if (length > 0) + { + length++; + var title = new StringBuilder(length); + User32.GetWindowText(hwnd, title, length); + var t = title.ToString(); + t = t.Trim(); + return t; + } + } + catch (Exception) + { + } //return hwnd.ToString("X8"); @@ -977,7 +1253,7 @@ namespace PersistentWindows.Common { return false; } - Log.Error("top left {0} is off-screen", topLeft.ToString()); + Log.Error($"top left {topLeft} is off-screen"); POINT topRight = new POINT(rect.Left + rect.Width - MinSize, rect.Top + MinSize); if (User32.MonitorFromPoint(topRight, User32.MONITOR_DEFAULTTONULL) != IntPtr.Zero) @@ -985,7 +1261,7 @@ namespace PersistentWindows.Common return false; } - Log.Error("top right {0} is off-screen", topRight.ToString()); + Log.Error($"top right {topRight} is off-screen"); return true; } @@ -1017,8 +1293,8 @@ namespace PersistentWindows.Common public bool RecallLastPositionKilledWindow(IntPtr hwnd) { - Int64 kid = FindMatchingKilledWindow(hwnd); - if (kid < 0) + IntPtr kid = FindMatchingKilledWindow(hwnd); + if (kid == IntPtr.Zero) return false; var d = deadApps[curDisplayKey][kid].Last(); @@ -1043,11 +1319,76 @@ namespace PersistentWindows.Common Log.Error("Restore last location \"{0}\"", GetWindowTitle(hwnd)); } - private void InheritKilledWindow(IntPtr hwnd, Int64 kid) + private void ResolveWindowHandleCollision(IntPtr hwnd) { - uint pid; - User32.GetWindowThreadProcessId(hwnd, out pid); + if (resolveHwndConflict) + { + try + { + ResolveWindowHandleCollisionCore(hwnd); + } catch (Exception ex) + { + Log.Error(ex.ToString()); + } + } + } + private void ResolveWindowHandleCollisionCore(IntPtr hwnd) + { + bool found_conflict = false; + string process_name = ""; + foreach (var display_key in deadApps.Keys) + { + if (deadApps[display_key].ContainsKey(hwnd)) + { + found_conflict = true; + process_name = deadApps[display_key][hwnd].Last().ProcessName; + + IntPtr fake_hwnd = (IntPtr)((fakeHwnd << 24) | (uint)hwnd); + if (fake_hwnd == hwnd) + continue; + + //replace prev zorder reference of dead hwnd with new fake_hwnd in monitorApplication + foreach (var hw in monitorApplications[display_key].Keys) + { + for (int i = 0; i < monitorApplications[display_key][hw].Count; i++) + { + if (monitorApplications[display_key][hw][i].PrevZorderWindow == hwnd) + monitorApplications[display_key][hw][i].PrevZorderWindow = fake_hwnd; + } + } + + //reindex + deadApps[display_key][fake_hwnd] = deadApps[display_key][hwnd]; + deadApps[display_key].Remove(hwnd); + + //replace prev zorder reference in deadApps as well + foreach (var kd in deadApps[display_key].Keys) + { + for (int i = 0; i < deadApps[display_key][kd].Count; i++) + { + if (deadApps[display_key][kd][i].PrevZorderWindow == hwnd) + deadApps[display_key][kd][i].PrevZorderWindow = fake_hwnd; + } + } + } + } + + if (found_conflict) + { + Log.Error($"Resolved window handle conflict between live and dead record {fakeHwnd} for {process_name}"); + fakeHwnd++; + } + } + + private ApplicationDisplayMetrics InheritKilledWindow(IntPtr hwnd, IntPtr realHwnd, IntPtr kid) + { + ApplicationDisplayMetrics r = null; + + uint pid; + User32.GetWindowThreadProcessId(realHwnd, out pid); + + lock(captureLock) foreach (var display_key in deadApps.Keys) { if (deadApps[display_key].ContainsKey(kid)) @@ -1058,64 +1399,215 @@ namespace PersistentWindows.Common deadApps[display_key][kid][i].ProcessId = pid; } - IntPtr dead_hwnd = deadApps[display_key][kid].Last().HWnd; + IntPtr dead_hwnd = kid; if (!monitorApplications.ContainsKey(display_key)) monitorApplications[display_key] = new Dictionary>(); + + List app_list = null; + if (monitorApplications[display_key].ContainsKey(hwnd)) + { + app_list = monitorApplications[display_key][hwnd]; + monitorApplications[display_key].Remove(hwnd); + } monitorApplications[display_key][hwnd] = deadApps[display_key][kid]; + if (app_list != null) + { + monitorApplications[display_key][hwnd].AddRange(app_list); + } deadApps[display_key].Remove(kid); - //replace prev zorder reference of dead_hwnd with hwnd + //replace prev zorder reference of dead_hwnd with hwnd in monitorApplication foreach (var hw in monitorApplications[display_key].Keys) { - if (hw == hwnd) - continue; - for (int i = 0; i < monitorApplications[display_key][hw].Count; i++) { if (monitorApplications[display_key][hw][i].PrevZorderWindow == dead_hwnd) monitorApplications[display_key][hw][i].PrevZorderWindow = hwnd; } + } + if (display_key == curDisplayKey) + r = monitorApplications[display_key][hwnd].Last(); + + //replace prev zorder reference in deadApps as well + foreach (var kd in deadApps[display_key].Keys) + { + for (int i = 0; i < deadApps[display_key][kd].Count; i++) + { + if (deadApps[display_key][kd][i].PrevZorderWindow == dead_hwnd) + deadApps[display_key][kd][i].PrevZorderWindow = hwnd; + } } } } + + return r; } - private Int64 FindMatchingKilledWindow(IntPtr hwnd) + int LenCommonPrefix(string a, string b) + { + int len = Math.Min(a.Length, b.Length); + int r; + for (r = 0; r < len; ++r) + { + if (a[r] != b[r]) + break; + } + return r; + } + + private IntPtr FindMatchingKilledWindow(IntPtr hwnd) { if (!deadApps.ContainsKey(curDisplayKey)) - return -1; + return IntPtr.Zero; - var deadAppPos = deadApps[curDisplayKey]; string className = GetWindowClassName(hwnd); + if (string.IsNullOrEmpty(className)) + return IntPtr.Zero; + + string procName; + string title; + if (className.Equals("ApplicationFrameWindow")) + { + //retrieve info about windows core app hidden under top window + IntPtr realHwnd = GetCoreAppWindow(hwnd); + if (!windowProcessName.ContainsKey(realHwnd)) + return IntPtr.Zero; + procName = windowProcessName[realHwnd]; + className = GetWindowClassName(realHwnd); + title = GetWindowTitle(realHwnd); + } + else + { + if (!windowProcessName.ContainsKey(hwnd)) + return IntPtr.Zero; + procName = windowProcessName[hwnd]; + title = GetWindowTitle(hwnd); + } + if (!string.IsNullOrEmpty(className)) { - string procName = windowProcessName[hwnd]; - string title = GetWindowTitle(hwnd); + int pos_match_cnt = 0; + IntPtr pos_match_hid = IntPtr.Zero; + int similar_pos_cnt = 0; + int diff_size = int.MaxValue; + IntPtr similar_pos_hid = IntPtr.Zero; + DateTime last_killed_time = new DateTime(0); + IntPtr last_killed_hid = IntPtr.Zero; + + long style = User32.GetWindowLong(hwnd, User32.GWL_STYLE); + long ext_style = User32.GetWindowLong(hwnd, User32.GWL_EXSTYLE); + + var deadAppPos = deadApps[curDisplayKey]; + lock(captureLock) foreach (var kid in deadAppPos.Keys) { - var appPos = deadAppPos[kid].Last(); - - if (!className.Equals(appPos.ClassName)) + var appPos = deadAppPos[kid].LastOrDefault(); + if (appPos == null) continue; + if (!procName.Equals(appPos.ProcessName)) continue; - // match position first + if (appPos.Style != 0 && style != appPos.Style) + continue; + if (appPos.ExtStyle != 0 && ext_style != appPos.ExtStyle) + continue; + + if (!className.Equals(appPos.ClassName)) + { + if (className.Length != appPos.ClassName.Length) + continue; + if (LenCommonPrefix(className, appPos.ClassName) < MinClassNamePrefix) + continue; + } + + if (IsMinimized(hwnd) != appPos.IsMinimized) + continue; + if (User32.IsWindowVisible(hwnd) == appPos.IsInvisible) + continue; + RECT r = appPos.ScreenPosition; RECT rect = new RECT(); User32.GetWindowRect(hwnd, ref rect); - if (rect.Equals(r)) + // find exact match first + if (rect.Equals(r) && title.Equals(appPos.Title)) return kid; - // lastly match title - if (title.Equals(appPos.Title)) - return kid; + if (rect.Equals(r)) + { + pos_match_cnt++; + pos_match_hid = kid; + } + + if (r.Diff(rect) < diff_size) + { + diff_size = r.Diff(rect); + similar_pos_cnt++; + similar_pos_hid = kid; + } + + if (appPos.CaptureTime > last_killed_time) + { + last_killed_time = appPos.CaptureTime; + last_killed_hid = kid; + } } + + if (pos_match_cnt == 1) + return pos_match_hid; + + if (diff_size <= MaxDiffPos) + { + Log.Event($"matching window with position diff of {diff_size}"); + return similar_pos_hid; + } + + if (!monitorApplications.ContainsKey(curDisplayKey)) + return IntPtr.Zero; + + int proc_name_match_cnt = 0; + int class_name_match_cnt = 0; + int class_name_mismatch_cnt = 0; + foreach(var h in monitorApplications[curDisplayKey].Keys) + { + foreach (var dm in monitorApplications[curDisplayKey][h]) + { + if (IsMinimized(hwnd) != dm.IsMinimized) + continue; + if (User32.IsWindowVisible(hwnd) == dm.IsInvisible) + continue; + + if (dm.ProcessName == procName) + { + proc_name_match_cnt++; + if (dm.ClassName == className) + class_name_match_cnt++; + else + class_name_mismatch_cnt++; + } + break; + } + } + + //force match last killed pos if hwnd is the first live window of the app + if (proc_name_match_cnt == 0) + return last_killed_hid; + + //force match most closest pos if hwnd is the first sub window of the app + if (proc_name_match_cnt == 1 && class_name_match_cnt == 0) + return similar_pos_hid; + + //force match if hwnd-like window has multiple instantiations but has only one top-level matching candidate + if (similar_pos_cnt == 1 && class_name_match_cnt > 0) + return similar_pos_hid; + + if (class_name_match_cnt > 0 && class_name_mismatch_cnt == 0) + return last_killed_hid; } - return -1; + return IntPtr.Zero; } private void FixOffScreenWindow(IntPtr hwnd) @@ -1209,7 +1701,7 @@ namespace PersistentWindows.Common { IntPtr topHwnd = User32.GetAncestor(hwnd, User32.GetAncestorRoot); if (hwnd == topHwnd) - HotKeyWindow.BrowserActivate(topHwnd); + HotKeyWindow.BrowserActivate(topHwnd, in_restore : restoringFromMem); } else HotKeyWindow.BrowserActivate(hwnd, false); @@ -1262,7 +1754,9 @@ namespace PersistentWindows.Common return; // unminimize to previous location + // RemoveInvalidCapture(hwnd); ApplicationDisplayMetrics prevDisplayMetrics = monitorApplications[curDisplayKey][hwnd].Last(); + var diff = prevDisplayMetrics.CaptureTime.Subtract(lastUnminimizeTime); if (diff.TotalMilliseconds > 0 && diff.TotalMilliseconds < 400) { @@ -1278,6 +1772,8 @@ namespace PersistentWindows.Common return; } + Log.Error("removed disqualified capture"); + prevDisplayMetrics = lastMetrics; } @@ -1289,6 +1785,7 @@ namespace PersistentWindows.Common //restore fullscreen window only applies if screen resolution has changed since minimize/normalize if (prevDisplayMetrics.CaptureTime < lastDisplayChangeTime) + lock(restoringFullScreenWindow) RestoreFullScreenWindow(hwnd, target_rect); return; } @@ -1305,6 +1802,7 @@ namespace PersistentWindows.Common || target_rect.Left <= -25600) { Log.Error("no qualified position data to restore minimized window \"{0}\"", GetWindowTitle(hwnd)); + Log.Error("{0}", prevDisplayMetrics); return; // captured without previous history info, let OS handle it } @@ -1371,6 +1869,15 @@ namespace PersistentWindows.Common || (style & (long)WindowStyleFlags.SYSMENU) != 0L; } + private static bool IsResizableWindow(IntPtr hwnd) + { + if (IsTaskBar(hwnd)) + return false; + + long style = User32.GetWindowLong(hwnd, User32.GWL_STYLE); + return (style & (long)WindowStyleFlags.THICKFRAME) != 0L; + } + private bool CaptureProcessName(IntPtr hwnd) { if (!windowProcessName.ContainsKey(hwnd)) @@ -1404,25 +1911,41 @@ namespace PersistentWindows.Common private void WinEventProc(IntPtr hWinEventHook, User32Events eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) { +#if DEBUG +#else + try +#endif + { + WinEventProcCore(hWinEventHook, eventType, hwnd, idObject, idChild, dwEventThread, dwmsEventTime); + } +#if DEBUG +#else + catch (Exception ex) + { + Log.Error(ex.ToString()); + } +#endif + } + + private void WinEventProcCore(IntPtr hWinEventHook, User32Events eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) + { if (!initialized) return; + if (hwnd == IntPtr.Zero) + return; + + if (idObject != 0) + // ignore non-window object (caret etc) + return; + { switch (eventType) { - case User32Events.EVENT_SYSTEM_MENUSTART: - case User32Events.EVENT_SYSTEM_MENUEND: - if (idObject == 0 || idObject == -1) - { - //TODO: - //context sensitive menu - } - break; case User32Events.EVENT_SYSTEM_MINIMIZEEND: case User32Events.EVENT_SYSTEM_MINIMIZESTART: case User32Events.EVENT_SYSTEM_MOVESIZEEND: - // only care about child windows that are moved by user - //normalSessions.Add(curDisplayKey); + // child windows are not captured by default unless moved by user allUserMoveWindows.Add(hwnd); break; @@ -1432,18 +1955,11 @@ namespace PersistentWindows.Common default: break; - //return; } } if (eventType == User32Events.EVENT_OBJECT_DESTROY) { - if (idObject != 0) - { - // ignore non-window object (caret etc) - return; - } - noRestoreWindows.Remove(hwnd); debugWindows.Remove(hwnd); if (fullScreenGamingWindows.Contains(hwnd)) @@ -1451,9 +1967,12 @@ namespace PersistentWindows.Common fullScreenGamingWindows.Remove(hwnd); exitFullScreenGaming = true; } + if (hwnd == fullScreenGamingWindow) + fullScreenGamingWindow = IntPtr.Zero; dualPosSwitchWindows.Remove(hwnd); bool found_history = false; + lock(captureLock) foreach (var display_config in monitorApplications.Keys) { if (!monitorApplications[display_config].ContainsKey(hwnd)) @@ -1466,22 +1985,36 @@ namespace PersistentWindows.Common // save window size of closed app to restore off-screen window later if (!deadApps.ContainsKey(display_config)) { - deadApps.Add(display_config, new Dictionary>()); + deadApps[display_config] = new Dictionary>(); } // for matching new window with killed one - monitorApplications[display_config][hwnd].Last().ProcessName = windowProcessName[hwnd]; + var dm = monitorApplications[display_config][hwnd].Last(); + if (dm.SnapShotFlags == 0) + dm.CaptureTime = DateTime.Now; //for inheritence in LIFO stile - deadApps[display_config][lastKilledWindowId] = monitorApplications[display_config][hwnd]; + deadApps[display_config][hwnd] = monitorApplications[display_config][hwnd]; windowTitle.Remove((IntPtr)monitorApplications[display_config][hwnd].Last().WindowId); + windowTitle.Remove(hwnd); //limit deadApp size - foreach (var kid in deadApps[display_config].Keys) + while (deadApps[display_config].Count > 1024) { - if (lastKilledWindowId - kid > 50) - deadApps[display_config].Remove(kid); - break; + var keys = deadApps[display_config].Keys; + DateTime tm = DateTime.Now; + IntPtr oldest_window = IntPtr.Zero; + foreach (var kid in keys) + { + DateTime t = deadApps[display_config][kid].Last().CaptureTime; + if (t < tm) + { + tm = t; + oldest_window = kid; + break; + } + } + deadApps[display_config].Remove(oldest_window); } } @@ -1491,16 +2024,10 @@ namespace PersistentWindows.Common if (found_history) { lastKillTime = DateTime.Now; - ++lastKilledWindowId; } windowProcessName.Remove(hwnd); - - bool found = windowTitle.Remove(hwnd); - if (sessionActive && found) - { - StartCaptureTimer(); //update z-order - } + windowTitle.Remove(hwnd); return; } @@ -1523,16 +2050,11 @@ namespace PersistentWindows.Common if (!CaptureProcessName(hwnd)) return; - if (ignoreProcess.Count > 0 || debugProcess.Count > 0) + if (ignoreProcess.Count > 0) { - string processName; - processName = windowProcessName[hwnd]; + string processName = windowProcessName[hwnd]; if (ignoreProcess.Contains(processName)) return; - if (debugProcess.Contains(processName)) - { - debugWindows.Add(hwnd); - } } #if DEBUG @@ -1542,13 +2064,6 @@ namespace PersistentWindows.Common { return; } - - /* - if (debugProcess.Count > 0 && !debugWindows.Contains(hwnd)) - { - return; - } - */ #endif // suppress capture for taskbar operation @@ -1562,8 +2077,9 @@ namespace PersistentWindows.Common { if (debugWindows.Contains(hwnd)) { - Log.Trace("WinEvent received. Type: {0:x4}, Window: {1:x8}", (uint)eventType, hwnd.ToInt64()); + Log.Event("WinEvent received {0} \"{1}\" {2:x4}", eventType, GetWindowTitle(hwnd), hwnd.ToInt32()); + #if DEBUG RECT screenPosition = new RECT(); User32.GetWindowRect(hwnd, ref screenPosition); var process = GetProcess(hwnd); @@ -1576,6 +2092,7 @@ namespace PersistentWindows.Common title ); Log.Trace(log); + #endif } if (restoringFromMem) @@ -1601,7 +2118,7 @@ namespace PersistentWindows.Common if (eventType == User32Events.EVENT_OBJECT_LOCATIONCHANGE) { - if ((remoteSession || restoreTimes >= MinRestoreTimes) && !restoringSnapshot) + if (((remoteSession && !restoreSingleWindow) || restoreTimes >= MinRestoreTimes) && !restoringSnapshot) { // restore is not finished as long as window location keeps changing CancelRestoreFinishedTimer(); @@ -1613,6 +2130,19 @@ namespace PersistentWindows.Common { switch (eventType) { + case User32Events.EVENT_OBJECT_CREATE: + { + if (restoringFromDB) + return; + + if (freezeCapture || !monitorApplications.ContainsKey(curDisplayKey)) + return; + + userMove = true; + StartCaptureTimer(UserMoveLatency / 4); + } + break; + case User32Events.EVENT_SYSTEM_FOREGROUND: { var cur_vdi = VirtualDesktop.GetWindowDesktopId(hwnd); @@ -1638,30 +2168,7 @@ namespace PersistentWindows.Common if (hwnd != vacantDeskWindow) foreGroundWindow = hwnd; - foregroundTimer.Change(100, Timeout.Infinite); - - // Occasionaly OS might bring a window to foreground upon sleep - // If the window move is initiated by OS (before sleep), - // keep restart capture timer would eventually discard these moves - // either by power suspend event handler calling CancelCaptureTimer() - // or due to capture timer handler found too many window moves - - // If the window move is caused by user snapping window to screen edge, - // delay capture by a few seconds should be fine. - - if (monitorApplications.ContainsKey(curDisplayKey) && monitorApplications[curDisplayKey].ContainsKey(hwnd)) - StartCaptureTimer(UserMoveLatency / 2); - else - { - StartCaptureTimer(); - - //speed up initial capture - POINT cursorPos; - User32.GetCursorPos(out cursorPos); - if (!cursorPos.Equals(initCursorPos)) - userMove = true; - } - + foregroundTimer.Change(ForegroundTimerLatency, Timeout.Infinite); } } @@ -1680,11 +2187,19 @@ namespace PersistentWindows.Common { if (hwnd != foreGroundWindow) pendingMoveEvents.Enqueue(hwnd); + else if (captureFloatingWindow) + allUserMoveWindows.Add(hwnd); } + if (fullScreenGamingWindow != IntPtr.Zero) + return; + + if (User32.IsZoomed(hwnd)) + userMove = true; + if (foreGroundWindow == hwnd) { - StartCaptureTimer(UserMoveLatency / 4); + StartCaptureTimer(UserMoveLatency); } else { @@ -1696,7 +2211,18 @@ namespace PersistentWindows.Common break; case User32Events.EVENT_SYSTEM_MOVESIZESTART: - curMovingWnd = hwnd; + if (freezeCapture) + { + Log.Event($"recognize {curDisplayKey} as user session"); + freezeCapture = false; //unlock unknown display session as normal + } + + if ((User32.GetKeyState(0x11) & 0x8000) != 0 //ctrl key pressed + && (User32.GetKeyState(0x10) & 0x8000) != 0) //shift key pressed + { + Log.Event("turn off auto-restore for window {0}", GetWindowTitle(hwnd)); + noRestoreWindows.Add(hwnd); + } break; case User32Events.EVENT_SYSTEM_MINIMIZEEND: @@ -1704,18 +2230,19 @@ namespace PersistentWindows.Common lastUnminimizeWindow = hwnd; tidyTabWindows.Remove(hwnd); //no longer hidden by tidytab + if (freezeCapture) + { + Log.Event($"recognize {curDisplayKey} as user session"); + freezeCapture = false; //unlock unknown display session as normal + } + if (monitorApplications.ContainsKey(curDisplayKey) && monitorApplications[curDisplayKey].ContainsKey(hwnd)) { //treat unminimized window as foreground realForeGroundWindow = hwnd; if (hwnd != vacantDeskWindow) foreGroundWindow = hwnd; - foregroundTimer.Change(100, Timeout.Infinite); - - //capture with slight delay inperceivable by user, required for full screen mode recovery - StartCaptureTimer(UserMoveLatency / 4); - Log.Trace("{0} {1}", eventType, GetWindowTitle(hwnd)); - userMove = true; + foregroundTimer.Change(ForegroundTimerLatency, Timeout.Infinite); } break; @@ -1737,11 +2264,18 @@ namespace PersistentWindows.Common if (enableMinimizeToTray) MinimizeToTray.Create(hwnd); + /* + if (freezeCapture) + { + Log.Event($"recognize {curDisplayKey} as user session"); + freezeCapture = false; //unlock unknown display session as normal + } + */ + goto case User32Events.EVENT_SYSTEM_MOVESIZEEND; case User32Events.EVENT_SYSTEM_MOVESIZEEND: if (eventType == User32Events.EVENT_SYSTEM_MOVESIZEEND) { - moveTimer.Change(100, Timeout.Infinite); if (!shift_key_pressed && !alt_key_pressed) { if (ctrl_key_pressed) @@ -1755,7 +2289,7 @@ namespace PersistentWindows.Common // only respond to move of captured window to avoid miscapture if (monitorApplications.ContainsKey(curDisplayKey) && monitorApplications[curDisplayKey].ContainsKey(hwnd) || allUserMoveWindows.Contains(hwnd)) { - StartCaptureTimer(UserMoveLatency / 4); + StartCaptureTimer(UserMoveLatency / 2); Log.Trace("{0} {1}", eventType, GetWindowTitle(hwnd)); userMove = true; } @@ -1771,28 +2305,52 @@ namespace PersistentWindows.Common private void TrimQueue(string displayKey, IntPtr hwnd) { + if (monitorApplications[displayKey][hwnd].Count > MaxHistoryQueueLength) + { + // limit length of snapshot capture history + ulong acc_flags = 0; + for (int i = monitorApplications[displayKey][hwnd].Count - 1; i >= 0; --i) + { + ulong snapshot_flags = monitorApplications[displayKey][hwnd][i].SnapShotFlags; + if (snapshot_flags != 0) + { + if ((snapshot_flags | acc_flags) == acc_flags) + { + Log.Event($"trim redundant snapshot record for {windowTitle[hwnd]}"); + monitorApplications[displayKey][hwnd].RemoveAt(i); + } + acc_flags |= snapshot_flags; + } + } + } + while (monitorApplications[displayKey][hwnd].Count > MaxHistoryQueueLength) { - // limit length of capture history + // limit length of non-snapshot capture history for (int i = 0; i < monitorApplications[displayKey][hwnd].Count; ++i) { - if (monitorApplications[displayKey][hwnd][i].SnapShotFlags != 0) - continue; //preserve snapshot record + ulong snapshot_flags = monitorApplications[displayKey][hwnd][i].SnapShotFlags; + if (snapshot_flags != 0) + continue; + + Log.Trace($"trim regular record for {windowTitle[hwnd]}"); monitorApplications[displayKey][hwnd].RemoveAt(i); - break; //remove one record at one time + break; //remove one record in each iteration } } } - private void RemoveInvalidCapture() + private void RemoveInvalidCapture(IntPtr h) { if (restoringSnapshot || restoringFromDB) return; if (monitorApplications.ContainsKey(curDisplayKey)) { - foreach (var hwnd in monitorApplications[curDisplayKey].Keys) + //foreach (var hwnd in monitorApplications[curDisplayKey].Keys) + if (monitorApplications[curDisplayKey].ContainsKey(h)) { + IntPtr hwnd = h; for (int i = monitorApplications[curDisplayKey][hwnd].Count - 1; i >= 0; --i) { if (!monitorApplications[curDisplayKey][hwnd][i].IsValid) @@ -1840,6 +2398,7 @@ namespace PersistentWindows.Common Log.Event("Snapshot {0} is captured", snapshotId); } + WriteDataDump(); return true; } @@ -1855,9 +2414,9 @@ namespace PersistentWindows.Common || !snapshotTakenTime[curDisplayKey].ContainsKey(id)) return; //snapshot not taken yet - if (id != MaxSnapshots - 1) + if (id < MaxSnapshots - 1) { - // MaxSnapshots - 1 is for undo snapshot restore + // MaxSnapshots - 1 is used for undo snapshot restore CaptureApplicationsOnCurrentDisplays(curDisplayKey, immediateCapture: true); snapshotTakenTime[curDisplayKey][MaxSnapshots - 1] = DateTime.Now; } @@ -1869,7 +2428,7 @@ namespace PersistentWindows.Common restoringSnapshot = true; snapshotId = id; restoringFromMem = true; - StartRestoreTimer(milliSecond: 0 /*wait mouse settle still for taskbar restore*/); + StartRestoreTimer(milliSecond: 0); Log.Event("restore snapshot {0}", id); } @@ -1894,6 +2453,9 @@ namespace PersistentWindows.Common if (IsMinimized(hWnd)) return IntPtr.Zero; + if (!monitorApplications.ContainsKey(curDisplayKey)) + return IntPtr.Zero; + RECT rect = new RECT(); User32.GetWindowRect(hWnd, ref rect); @@ -1908,6 +2470,8 @@ namespace PersistentWindows.Common break; if (result == result_prev) break; + if (result == HotKeyWindow.commanderWnd) + continue; if (monitorApplications[curDisplayKey].ContainsKey(result)) { @@ -2001,8 +2565,11 @@ namespace PersistentWindows.Common return fixZorder == 2 || (restoringSnapshot && fixZorder > 0); } - public static IntPtr GetForegroundWindow() + public static IntPtr GetForegroundWindow(bool strict = false) { + if (strict) + return User32.GetForegroundWindow(); + IntPtr topMostWindow = User32.GetTopWindow(desktopWindow); for (IntPtr hwnd = topMostWindow; hwnd != IntPtr.Zero; hwnd = User32.GetWindow(hwnd, 2)) { @@ -2048,15 +2615,18 @@ namespace PersistentWindows.Common Log.Event("Bring foreground window {0} to bottom", GetWindowTitle(hwnd)); } - public void SwitchForeBackground(IntPtr hwnd, bool toForeground=false, bool updateBackgroundPos=false, bool secondBackGround = false) + public void SwitchForeBackground(IntPtr hwnd, bool strict_dps_check = true, bool toForeground=false, bool updateBackgroundPos=false) { if (hwnd == IntPtr.Zero || IsTaskBar(hwnd)) return; if (!enableDualPosSwitch) return; - if (!dualPosSwitchWindows.Contains(hwnd)) - return; + if (strict_dps_check) + { + if (!dualPosSwitchWindows.Contains(hwnd)) + return; + } if (!monitorApplications.ContainsKey(curDisplayKey) || !monitorApplications[curDisplayKey].ContainsKey(hwnd)) return; @@ -2070,8 +2640,6 @@ namespace PersistentWindows.Common if (toForeground && IsTaskBar(front_hwnd)) return; //already foreground - IntPtr firstBackgroundWindow = IntPtr.Zero; - for (; prevIndex >= 0; --prevIndex) { var metrics = monitorApplications[curDisplayKey][hwnd][prevIndex]; @@ -2080,8 +2648,16 @@ namespace PersistentWindows.Common continue; } + if (!toForeground) + { + RECT screenPosition = new RECT(); + User32.GetWindowRect(hwnd, ref screenPosition); + if (screenPosition.Equals(metrics.ScreenPosition)) + continue; + } + IntPtr prevZwnd = metrics.PrevZorderWindow; - if (prevZwnd != front_hwnd && (prevZwnd == IntPtr.Zero || prevZwnd != firstBackgroundWindow)) + if (prevZwnd != front_hwnd) { if (toForeground) { @@ -2090,12 +2666,6 @@ namespace PersistentWindows.Common } else { - if (secondBackGround) - { - firstBackgroundWindow = prevZwnd; - secondBackGround = false; - continue; - } if (IsTaskBar(front_hwnd) && IsTaskBar(prevZwnd)) return; //#266, ignore taskbar (as prev-zwindow) change due to window maximize @@ -2147,7 +2717,7 @@ namespace PersistentWindows.Common | SetWindowPosFlags.IgnoreResize ); - Log.Event("Restore zorder {2} by repositioning window \"{0}\" under \"{1}\"", + Log.Trace("Restore zorder {2} by repositioning window \"{0}\" under \"{1}\"", GetWindowTitle(hWnd), GetWindowTitle(prev), ok ? "succeeded" : "failed"); @@ -2171,35 +2741,45 @@ namespace PersistentWindows.Common ApplicationDisplayMetrics prevDisplayMetrics; if (IsWindowMoved(displayKey, hWnd, eventType, now, out curDisplayMetrics, out prevDisplayMetrics)) { - if (debugWindows.Contains(hWnd)) - { - string log = string.Format("Captured {0,-8} at {1} '{2}' fullscreen:{3} minimized:{4}", - curDisplayMetrics, - curDisplayMetrics.ScreenPosition.ToString(), - curDisplayMetrics.Title, - curDisplayMetrics.IsFullScreen, - curDisplayMetrics.IsMinimized - ); - Log.Event(log); - - string log2 = string.Format(" WindowPlacement.NormalPosition at {0}", - curDisplayMetrics.WindowPlacement.NormalPosition.ToString()); - Log.Event(log2); - } - bool new_window = !monitorApplications[displayKey].ContainsKey(hWnd); if (eventType != 0 || new_window) curDisplayMetrics.IsValid = true; if (new_window) { - monitorApplications[displayKey].Add(hWnd, new List()); + //if (windowProcessName[hWnd] == "mstsc" && curDisplayMetrics.IsMinimized && curDisplayMetrics.IsInvisible && !curDisplayMetrics.IsFullScreen) + if (curDisplayMetrics.IsMinimized && curDisplayMetrics.IsInvisible && !curDisplayMetrics.IsFullScreen) + return false; //postpone capture till window is visible + + IntPtr kid = FindMatchingKilledWindow(hWnd); + TryInheritWindow(hWnd, curDisplayMetrics.HWnd, kid, curDisplayMetrics); + + if (kid == IntPtr.Zero) + monitorApplications[displayKey].Add(hWnd, new List()); } else { TrimQueue(displayKey, hWnd); } + if (debugWindows.Contains(hWnd)) + { + string log = string.Format("Captured {0} '{1}' fullscreen:{2} minimized:{3} visible:{4} at {5} {6, -8}", + curDisplayMetrics.HWnd.ToString("X"), + curDisplayMetrics.Title, + curDisplayMetrics.IsFullScreen, + curDisplayMetrics.IsMinimized, + !curDisplayMetrics.IsInvisible, + curDisplayMetrics.ScreenPosition.ToString(), + curDisplayMetrics + ); + Log.Event(log); + + string log2 = string.Format(" WindowPlacement.NormalPosition at {0}", + curDisplayMetrics.WindowPlacement.NormalPosition.ToString()); + Log.Event(log2); + } + monitorApplications[displayKey][hWnd].Add(curDisplayMetrics); ret = true; } @@ -2223,16 +2803,17 @@ namespace PersistentWindows.Common private void StartCaptureTimer(int milliSeconds = CaptureLatency) { + // ignore defer timer request to capture user move ASAP + if (captureTimerStarted && milliSeconds > UserMoveLatency) + return; + captureTimerStarted = true; + if (UserForcedCaptureLatency > 0) { captureTimer.Change(UserForcedCaptureLatency, Timeout.Infinite); return; } - // ignore defer timer request to capture user move ASAP - if (userMove) - return; //assuming timer has already started - // restart capture timer captureTimer.Change(milliSeconds, Timeout.Infinite); } @@ -2242,12 +2823,19 @@ namespace PersistentWindows.Common userMove = false; userMovePrev = false; + captureTimerStarted = false; + // restart capture timer captureTimer.Change(Timeout.Infinite, Timeout.Infinite); } public void StartRestoreTimer(int milliSecond = RestoreLatency) { + if (UserForcedRestoreLatency > RestoreLatency) + { + if (!restoringFromDB && !restoringSnapshot) + milliSecond = UserForcedCaptureLatency; + } restoreTimer.Change(milliSecond, Timeout.Infinite); } @@ -2270,6 +2858,14 @@ namespace PersistentWindows.Common { try { + if (exitFullScreenGaming) + return; + foreach (var hwnd in fullScreenGamingWindows) + { + if (IsFullScreen(hwnd)) + return; + } + if (restoringFromMem) { return; @@ -2286,9 +2882,8 @@ namespace PersistentWindows.Common { normalSessions.Add(curDisplayKey); Log.Trace("normal session {0} due to user move", curDisplayKey, userMovePrev); + CaptureApplicationsOnCurrentDisplays(displayKey, saveToDB: saveToDB); //implies auto delayed capture } - - CaptureApplicationsOnCurrentDisplays(displayKey, saveToDB: saveToDB); //implies auto delayed capture } catch (Exception ex) { @@ -2331,9 +2926,11 @@ namespace PersistentWindows.Common monitorApplications[displayKey][hwnd].Last().IsValid = true; } - if (lastUserActionTime.ContainsKey(displayKey)) - lastUserActionTimeBackup[displayKey] = lastUserActionTime[displayKey]; - lastUserActionTime[displayKey] = time; + if (!snapshotTakenTime.ContainsKey(displayKey)) + snapshotTakenTime[displayKey] = new Dictionary(); + if (snapshotTakenTime[displayKey].ContainsKey(MaxSnapshots)) + snapshotTakenTime[displayKey][MaxSnapshots + 1] = snapshotTakenTime[displayKey][MaxSnapshots]; + snapshotTakenTime[displayKey][MaxSnapshots] = time; Log.Trace("Capture time {0}", time); } @@ -2355,6 +2952,24 @@ namespace PersistentWindows.Common return null; } + private void UndoCapture(DateTime ref_time) + { + // rewind disqualified capture time + if (snapshotTakenTime.ContainsKey(curDisplayKey) && snapshotTakenTime[curDisplayKey].ContainsKey(MaxSnapshots)) + { + var lastCaptureTime = snapshotTakenTime[curDisplayKey][MaxSnapshots]; + var diff = ref_time - lastCaptureTime; + if (diff.TotalMilliseconds < CaptureLatency) + { + if (snapshotTakenTime[curDisplayKey].ContainsKey(MaxSnapshots + 1)) + { + snapshotTakenTime[curDisplayKey][MaxSnapshots] = snapshotTakenTime[curDisplayKey][MaxSnapshots + 1]; + Log.Error("undo capture of {0} at {1}", curDisplayKey, lastCaptureTime); + } + } + } + } + private void CaptureApplicationsOnCurrentDisplays(string displayKey, bool saveToDB = false, bool immediateCapture = false) { User32.SetThreadDpiAwarenessContextSafe(User32.DPI_AWARENESS_CONTEXT_UNAWARE); @@ -2473,7 +3088,7 @@ namespace PersistentWindows.Common } else if (displayKey.Equals(curDisplayKey)) { - if (movedWindows > 0) + if (!initialized || movedWindows > 0) { // confirmed user moves RecordLastUserActionTime(time: DateTime.Now, displayKey: displayKey); @@ -2520,7 +3135,8 @@ namespace PersistentWindows.Common //vacantDeskWindow = User32.FindWindowEx(vacantDeskWindow, IntPtr.Zero, "SHELLDLL_DefView", ""); //vacantDeskWindow = User32.FindWindowEx(vacantDeskWindow, IntPtr.Zero, "SysListView32", "FolderView"); //show icon on taskbar - hideRestoreTip(); + if (hideRestoreTip != null) + hideRestoreTip(); } continue; } @@ -2578,6 +3194,46 @@ namespace PersistentWindows.Common return true; } + private bool TryInheritWindow(IntPtr hwnd, IntPtr realHwnd, IntPtr kid, ApplicationDisplayMetrics curDisplayMetrics) + { + if (kid == IntPtr.Zero) + { + ResolveWindowHandleCollision(hwnd); + } + else + { + var prevDisplayMetrics = InheritKilledWindow(hwnd, realHwnd, kid); + if (hwnd != kid) + { + if (prevDisplayMetrics.Title != curDisplayMetrics.Title) + Log.Error($"{hwnd.ToString("X")} Inherit position data from killed window {prevDisplayMetrics.Title} with different title {curDisplayMetrics.Title} {prevDisplayMetrics.HWnd.ToString("X")}"); + else + Log.Error($"{hwnd.ToString("X")} Inherit position data from killed window {prevDisplayMetrics.Title} {prevDisplayMetrics.HWnd.ToString("X")}"); + ResolveWindowHandleCollision(hwnd); + } + else + Log.Error($"{hwnd.ToString("X")} Inherit position data from existing window 0x{kid.ToString("X")} for {curDisplayMetrics.Title}"); + + if (initialized && autoRestoreNewWindowToLastCapture) + { + if (!restoringFromDB && IsResizableWindow(hwnd)) + { + Log.Trace($"restore {windowTitle[hwnd]} to last captured position"); + restoreSingleWindow = true; + restoringFromMem = true; + RestoreApplicationsOnCurrentDisplays(curDisplayKey, hwnd, prevDisplayMetrics.CaptureTime); + restoreSingleWindow = false; + restoringFromMem = false; + userMove = true; + StartCaptureTimer(UserMoveLatency / 2); + } + } + return true; + } + + return false; + + } private bool IsWindowMoved(string displayKey, IntPtr hwnd, User32Events eventType, DateTime time, out ApplicationDisplayMetrics curDisplayMetrics, out ApplicationDisplayMetrics prevDisplayMetrics) { @@ -2590,6 +3246,9 @@ namespace PersistentWindows.Common return false; } + if (hwnd == HotKeyWindow.commanderWnd) + return false; + bool isTaskBar = false; if (IsTaskBar(hwnd)) { @@ -2619,6 +3278,18 @@ namespace PersistentWindows.Common if (!CaptureProcessName(realHwnd)) return false; + if (debugProcess.Count > 0) + { + if (windowProcessName.ContainsKey(hwnd)) + { + string processName = windowProcessName[hwnd]; + if (debugProcess.Contains(processName)) + { + debugWindows.Add(hwnd); + } + } + } + bool isFullScreen = IsFullScreen(hwnd); curDisplayMetrics = new ApplicationDisplayMetrics @@ -2642,6 +3313,9 @@ namespace PersistentWindows.Common NeedUpdateWindowPlacement = false, ScreenPosition = screenPosition, + Style = User32.GetWindowLong(hwnd, User32.GWL_STYLE), + ExtStyle = User32.GetWindowLong(hwnd, User32.GWL_EXSTYLE), + IsTopMost = IsWindowTopMost(hwnd), NeedClearTopMost = false, @@ -2658,13 +3332,6 @@ namespace PersistentWindows.Common if (noRestoreWindows.Contains(hwnd)) return false; - Int64 kid = FindMatchingKilledWindow(hwnd); - if (kid >= 0) - { - InheritKilledWindow(hwnd, kid); - Log.Error($"Inherit position data from killed window {kid} for {curDisplayMetrics.Title}"); - } - //newly created window or new display setting curDisplayMetrics.WindowId = (uint)realHwnd; @@ -2683,7 +3350,10 @@ namespace PersistentWindows.Common } } - moved = true; + if (curDisplayMetrics.IsMinimized && prevDisplayMetrics != null && prevDisplayMetrics.IsMinimized) + moved = false; + else + moved = true; } else if (!monitorApplications[displayKey].ContainsKey(hwnd)) { @@ -2711,9 +3381,8 @@ namespace PersistentWindows.Common if (prevIndex < 0) { Log.Error("no previous record found for window {0}", GetWindowTitle(hwnd)); - noRecordWindows.Add(hwnd); - if (restoringFromMem && monitorApplications[displayKey][hwnd].Count < 2) + if (restoringSnapshot) { //the window did not exist when snapshot was taken User32.SetWindowPos(hwnd, new IntPtr(1), //bottom @@ -2729,7 +3398,11 @@ namespace PersistentWindows.Common return !restoringFromMem; } + //update title even if window is not moved prevDisplayMetrics = monitorApplications[displayKey][hwnd][prevIndex]; + if (prevDisplayMetrics.Title != curDisplayMetrics.Title) + prevDisplayMetrics.Title = curDisplayMetrics.Title; + curDisplayMetrics.Id = prevDisplayMetrics.Id; //curDisplayMetrics.ProcessName = prevDisplayMetrics.ProcessName; curDisplayMetrics.WindowId = prevDisplayMetrics.WindowId; @@ -2835,7 +3508,7 @@ namespace PersistentWindows.Common return moved; } - private void TimerRestore() + private void TimerRestore(object state) { if (pauseAutoRestore && !restoringFromDB && !restoringSnapshot) return; @@ -2847,7 +3520,6 @@ namespace PersistentWindows.Common normalSessions.Add(curDisplayKey); Log.Trace("Restore timer expired"); - process.PriorityClass = ProcessPriorityClass.High; lock (restoreLock) @@ -2882,7 +3554,7 @@ namespace PersistentWindows.Common try { - RemoveInvalidCapture(); + RemoveInvalidCapture(IntPtr.Zero); extra_restore = RestoreApplicationsOnCurrentDisplays(displayKey, IntPtr.Zero, DateTime.Now); } catch (Exception ex) @@ -2977,10 +3649,17 @@ namespace PersistentWindows.Common private void RestoreFullScreenWindow(IntPtr hwnd, RECT target_rect) { + int double_clck_interval = System.Windows.Forms.SystemInformation.DoubleClickTime / 2; long style = User32.GetWindowLong(hwnd, User32.GWL_STYLE); if ((style & (long)WindowStyleFlags.CAPTION) == 0L) { - return; + Thread.Sleep(3 * double_clck_interval); + style = User32.GetWindowLong(hwnd, User32.GWL_STYLE); + if ((style & (long)WindowStyleFlags.CAPTION) == 0L) + { + Log.Error("no need to restore full screen window {0}", GetWindowTitle(hwnd)); + return; + } /* style |= (long)WindowStyleFlags.CAPTION; User32.SetWindowLong(hwnd, User32.GWL_STYLE, style); @@ -3013,30 +3692,47 @@ namespace PersistentWindows.Common int centerx = screenPosition.Left + screenPosition.Width / 8; int centery = screenPosition.Top + 15; - //User32.SetActiveWindow(hwnd); User32.SetCursorPos(centerx, centery); - int double_clck_interval = System.Windows.Forms.SystemInformation.DoubleClickTime / 2; User32.mouse_event(MouseAction.MOUSEEVENTF_LEFTDOWN | MouseAction.MOUSEEVENTF_LEFTUP, 0, 0, 0, UIntPtr.Zero); Thread.Sleep(double_clck_interval); User32.mouse_event(MouseAction.MOUSEEVENTF_LEFTDOWN | MouseAction.MOUSEEVENTF_LEFTUP, 0, 0, 0, UIntPtr.Zero); - Thread.Sleep(double_clck_interval); + Log.Error("restore full screen window {0}", GetWindowTitle(hwnd)); + + /* + Thread.Sleep(3 * double_clck_interval); style = User32.GetWindowLong(hwnd, User32.GWL_STYLE); if ((style & (long)WindowStyleFlags.CAPTION) == 0L) { - Log.Error("restore full screen window {0}", GetWindowTitle(hwnd)); return; } User32.mouse_event(MouseAction.MOUSEEVENTF_LEFTDOWN | MouseAction.MOUSEEVENTF_LEFTUP, 0, 0, 0, UIntPtr.Zero); + Thread.Sleep(double_clck_interval); + User32.mouse_event(MouseAction.MOUSEEVENTF_LEFTDOWN | MouseAction.MOUSEEVENTF_LEFTUP, + 0, 0, 0, UIntPtr.Zero); + Log.Error("double restore full screen window {0}", GetWindowTitle(hwnd)); + + Thread.Sleep(3 * double_clck_interval); + style = User32.GetWindowLong(hwnd, User32.GWL_STYLE); + if ((style & (long)WindowStyleFlags.CAPTION) == 0L) + { + return; + } + + Log.Error("fail to restore full screen window {0}", GetWindowTitle(hwnd)); + */ } private void RestoreSnapWindow(IntPtr hwnd, RECT target_pos) { + if (!IsResizableWindow(hwnd)) + return; + List displays = GetDisplays(); foreach (var display in displays) { @@ -3105,13 +3801,16 @@ namespace PersistentWindows.Common if (intersect.Equals(sourceRect) || intersect.Equals(targetRect)) return false; //only taskbar size changes - Log.Event($"move taskbar to {targetRect}"); + /* + if (sourceRect.Width != targetRect.Width && sourceRect.Height != targetRect.Height) + { + Log.Error("wait taskbar stabilize"); + return false; + } + */ - //IntPtr hReBar = User32.FindWindowEx(hwnd, IntPtr.Zero, "ReBarWindow32", null); - //User32.GetWindowRect(hReBar, ref screenPosition); + Log.Event($"move taskbar from {sourceRect} to {targetRect}"); - //IntPtr hTaskBar = User32.FindWindowEx(hReBar, IntPtr.Zero, "MSTaskSwWClass", null); - //hTaskBar = User32.FindWindowEx(hTaskBar, IntPtr.Zero, "MSTaskListWClass", null); IntPtr hTaskBar = GetRealTaskBar(hwnd); User32.GetWindowRect(hTaskBar, ref sourceRect); @@ -3160,15 +3859,52 @@ namespace PersistentWindows.Common return true; } + // 3 edges of taskbar are aligned to screen border + private bool IsTaskbarAligned(IntPtr hwnd) + { + int x_aligned = 0; + int y_aligned = 0; + RECT rect = new RECT(); + User32.GetWindowRect(hwnd, ref rect); + + RECT intersect = new RECT(); + List displays = GetDisplays(); + foreach (var display in displays) + { + RECT screen = display.Position; + if (User32.IntersectRect(out intersect, ref rect, ref screen)) + { + if (Math.Abs(rect.Left - screen.Left) < 5) + x_aligned += 1; + if (Math.Abs(rect.Right - screen.Right) < 5) + x_aligned += 1; + if (Math.Abs(rect.Top - screen.Top) < 5) + y_aligned += 1; + if (Math.Abs(rect.Bottom - screen.Bottom) < 5) + y_aligned += 1; + break; + } + } + + return x_aligned + y_aligned == 3; + } + // recover height of horizontal taskbar or width of vertical taskbar private bool RecoverTaskBarArea(IntPtr hwnd, RECT targetRect) { + if (remoteSession) + { + //attempt to restore taskbar size only once for rdp session + if (restoreTimes != 2) + return false; + } + RECT sourceRect = new RECT(); User32.GetWindowRect(hwnd, ref sourceRect); int deltaWidth = sourceRect.Width - targetRect.Width; int deltaHeight = sourceRect.Height - targetRect.Height; - if (Math.Abs(deltaWidth) < 10 && Math.Abs(deltaHeight) < 10) + if (Math.Abs(deltaWidth) < 25 && Math.Abs(deltaHeight) < 25) return false; RECT intersect = new RECT(); @@ -3193,8 +3929,6 @@ namespace PersistentWindows.Common } } - Log.Error("restore width of taskbar window {0}", GetWindowTitle(hwnd)); - int start_y; int start_x; int end_x = -25600; @@ -3202,6 +3936,8 @@ namespace PersistentWindows.Common if (deltaWidth != 0) { //restore width + Log.Error("restore width of taskbar window {0}", GetWindowTitle(hwnd)); + start_y = sourceRect.Top + sourceRect.Height / 2; if (left_edge) { @@ -3219,6 +3955,8 @@ namespace PersistentWindows.Common else { //restore height + Log.Error("restore height of taskbar window {0}", GetWindowTitle(hwnd)); + start_x = sourceRect.Left + sourceRect.Width / 2; if (top_edge) { @@ -3313,20 +4051,18 @@ namespace PersistentWindows.Common sWindows = CaptureWindowsOfInterest(); // determine the time to be restored - if (lastUserActionTime.ContainsKey(displayKey)) + if (restoringSnapshot) { - if (restoringSnapshot) - { - if (!snapshotTakenTime.ContainsKey(curDisplayKey) - || !snapshotTakenTime[curDisplayKey].ContainsKey(snapshotId)) - return false; + if (!snapshotTakenTime.ContainsKey(curDisplayKey) + || !snapshotTakenTime[curDisplayKey].ContainsKey(snapshotId)) + return false; - lastCaptureTime = snapshotTakenTime[curDisplayKey][snapshotId]; - } - else - { - lastCaptureTime = lastUserActionTime[displayKey]; - } + lastCaptureTime = snapshotTakenTime[curDisplayKey][snapshotId]; + } + else if (snapshotTakenTime.ContainsKey(curDisplayKey) + && snapshotTakenTime[curDisplayKey].ContainsKey(MaxSnapshots)) + { + lastCaptureTime = snapshotTakenTime[displayKey][MaxSnapshots]; } } @@ -3555,10 +4291,18 @@ namespace PersistentWindows.Common if (IsTaskBar(hWnd)) { - if (!fixTaskBar && !restoringFromDB && !restoringSnapshot) - continue; + if (fixTaskBar == 0 && !restoringFromDB && !restoringSnapshot) + continue; //auto restore taskbar disabled - if (fullScreenGamingWindows.Count > 0 || exitFullScreenGaming) + if (!IsTaskbarAligned(hWnd)) + { + //not ready to drag + Log.Error("Taskbar not aligned"); + continue; + } + + if (fixTaskBar == -1) //disable possible bogus taskbar restore after game play due to inaccurate position report + if (fullScreenGamingWindow != IntPtr.Zero || fullScreenGamingWindows.Count > 0 || exitFullScreenGaming) continue; int taskbarMovable = (int)Registry.GetValue(@"HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced", "TaskbarSizeMove", 1); @@ -3566,8 +4310,10 @@ namespace PersistentWindows.Common { User32.SendMessage(hWnd, User32.WM_COMMAND, User32.SC_TOGGLE_TASKBAR_LOCK, null); } + bool changed_edge = MoveTaskBar(hWnd, rect); bool changed_width = RecoverTaskBarArea(hWnd, rect); + if (changed_edge || changed_width) restoredWindows.Add(hWnd); if (taskbarMovable == 0) @@ -3651,6 +4397,7 @@ namespace PersistentWindows.Common } } + bool resizable = IsResizableWindow(hWnd); if (curDisplayMetrics.NeedUpdateWindowPlacement) { // recover NormalPosition (the workspace position prior to snap) @@ -3695,7 +4442,7 @@ namespace PersistentWindows.Common } } - if (need_move_window) + if (need_move_window && resizable) { success &= User32.SetWindowPlacement(hWnd, ref windowPlacement); } @@ -3706,7 +4453,14 @@ namespace PersistentWindows.Common { if (need_move_window) { - success &= User32.MoveWindow(hWnd, rect.Left, rect.Top, rect.Width, rect.Height, true); + if (resizable) + success &= User32.MoveWindow(hWnd, rect.Left, rect.Top, rect.Width, rect.Height, true); + else + { + Log.Error($"keep window size for floating window {GetWindowTitle(hWnd)}"); + success &= User32.MoveWindow(hWnd, rect.Left, rect.Top, curDisplayMetrics.ScreenPosition.Width, curDisplayMetrics.ScreenPosition.Height, true); + } + if (debugWindows.Contains(hWnd)) Log.Event("MoveWindow({0} [{1}x{2}]-[{3}x{4}]) - {5}", prevDisplayMetrics.ProcessName, @@ -3720,6 +4474,7 @@ namespace PersistentWindows.Common if (restore_fullscreen) { if (restoreTimes > 0 && sWindow == null) //#246, let other windows restore first + lock(restoringFullScreenWindow) RestoreFullScreenWindow(hWnd, rect); } else if (restoreTimes >= MinRestoreTimes - 1) @@ -3744,7 +4499,6 @@ namespace PersistentWindows.Common if (batchZorderFix) { HashSet risky_windows = unResponsiveWindows; - risky_windows.IntersectWith(noRecordWindows); if (risky_windows.Count == 0) try { @@ -3956,6 +4710,10 @@ namespace PersistentWindows.Common File.WriteAllText(batFile, "start \"\" /B " + dir); } + else if (dir.Equals("This PC") || dir.Equals("Computer")) + { + File.WriteAllText(batFile, "explorer /n, /select, %SystemDrive%"); + } else { string home = System.Environment.GetEnvironmentVariable("USERPROFILE"); @@ -4010,7 +4768,7 @@ namespace PersistentWindows.Common } - private string GetProcExePath(uint proc_id) + public static string GetProcExePath(uint proc_id) { IntPtr hProcess = Kernel32.OpenProcess(Kernel32.ProcessAccessFlags.QueryInformation, false, proc_id); string pathToExe = string.Empty; @@ -4171,17 +4929,18 @@ namespace PersistentWindows.Common } } -#region IDisposable - public virtual void Dispose(bool disposing) + public void Stop() { - StopRunningThreads(); - if (initialized) { + initialized = false; + EndDisplaySession(); + SystemEvents.DisplaySettingsChanging -= displaySettingsChangingHandler; SystemEvents.DisplaySettingsChanged -= displaySettingsChangedHandler; SystemEvents.PowerModeChanged -= powerModeChangedHandler; SystemEvents.SessionSwitch -= sessionSwitchEventHandler; + SystemEvents.SessionEnding -= sessionEndingEventHandler; foreach (var handle in this.winEventHooks) { @@ -4190,6 +4949,13 @@ namespace PersistentWindows.Common } } +#region IDisposable + public virtual void Dispose(bool disposing) + { + Stop(); + StopRunningThreads(); + } + public void Dispose() { Dispose(true); diff --git a/Ninjacrab.PersistentWindows.Solution/Common/WinApiBridge/User32.cs b/Ninjacrab.PersistentWindows.Solution/Common/WinApiBridge/User32.cs index 139284c..b2e453d 100644 --- a/Ninjacrab.PersistentWindows.Solution/Common/WinApiBridge/User32.cs +++ b/Ninjacrab.PersistentWindows.Solution/Common/WinApiBridge/User32.cs @@ -41,6 +41,7 @@ namespace PersistentWindows.Common.WinApiBridge EVENT_SYSTEM_IME_KEY_NOTIFICATION = 0x0029, EVENT_SYSTEM_END = 0x00FF, + EVENT_OBJECT_CREATE = 0x8000, EVENT_OBJECT_DESTROY = 0x8001, EVENT_OBJECT_REORDER = 0x8004, EVENT_OBJECT_LOCATIONCHANGE = 0x800B, @@ -623,6 +624,9 @@ namespace PersistentWindows.Common.WinApiBridge [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(IntPtr hHandle); + [DllImport("kernel32")] + public static extern UInt64 GetTickCount64(); + [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr OpenProcess( ProcessAccessFlags processAccess, diff --git a/Ninjacrab.PersistentWindows.Solution/Common/WinApiBridge/WindowsPosition.cs b/Ninjacrab.PersistentWindows.Solution/Common/WinApiBridge/WindowsPosition.cs index 19a632a..521b285 100644 --- a/Ninjacrab.PersistentWindows.Solution/Common/WinApiBridge/WindowsPosition.cs +++ b/Ninjacrab.PersistentWindows.Solution/Common/WinApiBridge/WindowsPosition.cs @@ -28,6 +28,10 @@ namespace PersistentWindows.Common.WinApiBridge X = x; Y = y; } + public override string ToString() + { + return string.Format($"({X}, {Y})"); + } } [StructLayout(LayoutKind.Sequential)] @@ -57,5 +61,11 @@ namespace PersistentWindows.Common.WinApiBridge { return string.Format("({0}, {1}), {2} x {3}", Left, Top, Width, Height); } + + public int Diff(RECT r) + { + int diff = Math.Abs(Left - r.Left) + Math.Abs(Right - r.Right) + Math.Abs(Top - r.Top) + Math.Abs(Bottom - r.Bottom); + return diff / 4; + } } } diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/HotKey.cs b/Ninjacrab.PersistentWindows.Solution/SystrayShell/HotKey.cs index 5363fe0..b541206 100644 --- a/Ninjacrab.PersistentWindows.Solution/SystrayShell/HotKey.cs +++ b/Ninjacrab.PersistentWindows.Solution/SystrayShell/HotKey.cs @@ -1,6 +1,8 @@ using System; using System.Threading; using System.Windows.Forms; +using System.Diagnostics; +using System.IO; using PersistentWindows.Common; using PersistentWindows.Common.WinApiBridge; @@ -10,6 +12,7 @@ namespace PersistentWindows.SystrayShell { public class HotKeyForm : Form { + static bool init = true; static HotKeyWindow hkwin = null; static Thread messageLoop; @@ -60,9 +63,42 @@ namespace PersistentWindows.SystrayShell User32.KeyModifier modifier = (User32.KeyModifier)((int)m.LParam & 0xFFFF); // The modifier of the hotkey that was pressed. int id = m.WParam.ToInt32(); // The id of the hotkey that was pressed. - Program.HideRestoreTip(false); //hide icon - Program.HideRestoreTip(); //show icon + IntPtr fgWnd = PersistentWindowProcessor.GetForegroundWindow(strict : true); hkwin.HotKeyPressed(from_menu : false); + if (PersistentWindowProcessor.IsBrowserWindow(fgWnd)) + { + Program.HideRestoreTip(false); //hide icon + Program.HideRestoreTip(); //show icon + + if (init) + { + init = false; + string webpage_commander_notification = Path.Combine(Program.AppdataFolder, "webpage_commander_notification"); + if (File.Exists(webpage_commander_notification)) + { + Program.systrayForm.notifyIconMain.ShowBalloonTip(8000, "webpage commander is invoked via hotkey", "Press the hotkey (Alt + W) again to revoke", ToolTipIcon.Info); + } + else + { + try + { + File.Create(webpage_commander_notification); + + uint processId; + User32.GetWindowThreadProcessId(fgWnd, out processId); + string procPath = PersistentWindowProcessor.GetProcExePath(processId); + Process.Start(procPath, Program.ProjectUrl + "/blob/master/webpage_commander.md"); + } + catch (Exception ex) + { + Log.Error(ex.ToString()); + Program.systrayForm.notifyIconMain.ShowBalloonTip(8000, "webpage commander is invoked via hotkey", "Press the hotkey (Alt + W) again to revoke", ToolTipIcon.Info); + Process.Start(Program.ProjectUrl + "/blob/master/webpage_commander.md"); + } + } + } + } + return; } else if (m.Msg == 0x0010 || m.Msg == 0x0002) diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Program.cs b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Program.cs index e1ef421..73168a9 100644 --- a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Program.cs +++ b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Program.cs @@ -21,15 +21,21 @@ namespace PersistentWindows.SystrayShell public static System.Drawing.Icon BusyIcon = null; public static System.Drawing.Icon UpdateIcon = null; public static string AppdataFolder = null; + public static string DisableWebpageCommander = null; + public static string DisableUpgradeNotice = null; public static string CmdArgs; public static bool Gui = true; public static bool hotkey_window = true; public static uint hotkey = 'W'; //Alt + W + public static string WaitPwFinish = @":wait_to_finish +timeout /t 2 /nobreak >nul +tasklist | find ""PersistentWindows"" >nul +if not errorlevel 1 goto wait_to_finish"; private const int MaxSnapshots = 38; // 0-9, a-z, ` and final one for undo - static PersistentWindowProcessor pwp = null; - static SystrayForm systrayForm = null; + public static PersistentWindowProcessor pwp = null; + public static SystrayForm systrayForm = null; static bool silent = false; //suppress all balloon tip & sound prompt static bool notification = false; //pop balloon when auto restore static int delay_manual_capture = 5000; //in millisecond @@ -50,9 +56,18 @@ namespace PersistentWindows.SystrayShell pwp = new PersistentWindowProcessor(); + var process = Process.GetCurrentProcess(); + pwp.processPriority = process.PriorityClass; + process.PriorityClass = ProcessPriorityClass.High; + var timer = new System.Threading.Timer(state => + { + process.PriorityClass = pwp.processPriority; + }); + timer.Change(10000, System.Threading.Timeout.Infinite); + bool splash = true; - int delay_start = 0; - bool relaunch = false; + int delay_restart = 0; + int relaunch_delay = 0; int delay_manual_capture = 0; int delay_auto_capture = 0; bool redirect_appdata = false; // use "." instead of appdata/local/PersistentWindows to store db file @@ -68,13 +83,16 @@ namespace PersistentWindows.SystrayShell bool offscreen_fix = true; bool fix_unminimized_window = true; bool enhanced_offscreen_fix = false; + bool set_pos_match_threshold = false; bool auto_restore_missing_windows = false; bool auto_restore_from_db_at_startup = false; + bool auto_restore_last_capture_at_startup = false; bool launch_once_per_process_id = true; bool check_upgrade = true; bool auto_upgrade = false; bool legacy_icon = false; bool waiting_taskbar = false; + int restore_snapshot = -1; foreach (var arg in args) { @@ -86,13 +104,12 @@ namespace PersistentWindows.SystrayShell pwp.haltRestore = (Int32)(float.Parse(arg) * 1000); continue; } - else if (delay_start != 0) + else if (delay_restart != 0) { - delay_start = 0; + delay_restart = 0; if (!waiting_taskbar) { - Thread.Sleep((Int32)(float.Parse(arg) * 1000)); - relaunch = true; + relaunch_delay = (Int32)(float.Parse(arg)); } continue; } @@ -131,6 +148,17 @@ namespace PersistentWindows.SystrayShell hotkey = arg[0]; continue; } + else if (restore_snapshot != -1) + { + restore_snapshot = SnapshotCharToId(arg[0]); + continue; + } + else if (set_pos_match_threshold) + { + set_pos_match_threshold = false; + pwp.MaxDiffPos = int.Parse(arg); + continue; + } switch(arg) { @@ -153,8 +181,11 @@ namespace PersistentWindows.SystrayShell case "-enable_auto_restore_by_manual_capture": pwp.manualNormalSession = true; break; - case "-delay_start": - delay_start = 1; + case "-fast_restore=0": + pwp.fastRestore = false; + break; + case "-delay_restart": + delay_restart = 1; break; case "-wait_taskbar": waiting_taskbar = true; @@ -165,6 +196,9 @@ namespace PersistentWindows.SystrayShell case "-delay_auto_capture": delay_auto_capture = 1; break; + case "-capture_floating_window=0": + pwp.captureFloatingWindow = false; + break; case "-dpi_sensitive_call=1": User32.DpiSenstiveCall = true; break; @@ -199,7 +233,10 @@ namespace PersistentWindows.SystrayShell fix_unminimized_window = false; break; case "-fix_taskbar=0": - pwp.fixTaskBar = false; + pwp.fixTaskBar = 0; + break; + case "-fix_taskbar_no_game": + pwp.fixTaskBar = -1; break; case "-foreground_background_dual_position=0": pwp.enableDualPosSwitch = false; @@ -239,6 +276,15 @@ namespace PersistentWindows.SystrayShell case "-redraw_desktop": redraw_desktop = true; break; + case "-auto_restore_existing_window_to_last_capture=1": + auto_restore_last_capture_at_startup = true; + break; + case "-auto_restore_new_window_to_last_capture=0": + pwp.autoRestoreNewWindowToLastCapture = false; + break; + case "-pos_match_threshold": + set_pos_match_threshold = true; + break; case "-auto_restore_missing_windows": case "-auto_restore_missing_windows=1": auto_restore_missing_windows = true; @@ -251,7 +297,7 @@ namespace PersistentWindows.SystrayShell auto_restore_missing_windows = true; break; case "-auto_restore_new_display_session_from_db=0": - pwp.autoRestoreLiveWindows = false; + pwp.autoRestoreLiveWindowsFromDb = false; Log.Error("turn off auto restore db for new session"); break; case "-invoke_multi_window_process_only_once=0": @@ -263,9 +309,21 @@ namespace PersistentWindows.SystrayShell case "-auto_upgrade=1": auto_upgrade = true; break; + case "-dump_window_position_history=0": + pwp.dumpHistoryData = false; + break; + case "-restore_snapshot": + restore_snapshot = 0; + break; } } + if (restore_snapshot >= 0) + { + pwp.RestoreSnapshotCmd(restore_snapshot); + return; + } + string productName = System.Windows.Forms.Application.ProductName; string appDataFolder = redirect_appdata ? "." : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @@ -278,11 +336,18 @@ namespace PersistentWindows.SystrayShell #endif AppdataFolder = appDataFolder; + if (!Directory.Exists(appDataFolder)) + Directory.CreateDirectory(appDataFolder); + + DisableWebpageCommander = Path.Combine(AppdataFolder, "disable_webpage_commander"); + DisableUpgradeNotice = Path.Combine(AppdataFolder, "disable_upgrade_notice"); + // default icons - IdleIcon = legacy_icon ? Properties.Resources.pwIcon2 : Properties.Resources.pwIcon; - var iconHandle = Properties.Resources.pwIconBusy.GetHicon(); - BusyIcon = legacy_icon ? Properties.Resources.pwIconBusy2 : System.Drawing.Icon.FromHandle(iconHandle); - iconHandle = Properties.Resources.pwIconUpdate.GetHicon(); + var iconHandle = (legacy_icon ? Properties.Resources.pwIcon2: Properties.Resources.pwIcon).GetHicon(); + IdleIcon = System.Drawing.Icon.FromHandle(iconHandle); + iconHandle = (legacy_icon ? Properties.Resources.pwIconBusy2 : Properties.Resources.pwIconBusy).GetHicon(); + BusyIcon = System.Drawing.Icon.FromHandle(iconHandle); + iconHandle = (legacy_icon ? Properties.Resources.pwIconUpdate2 : Properties.Resources.pwIconUpdate).GetHicon(); UpdateIcon = System.Drawing.Icon.FromHandle(iconHandle); // customized icon/png @@ -291,38 +356,49 @@ namespace PersistentWindows.SystrayShell if (i == 1) iconFolder = AppDomain.CurrentDomain.BaseDirectory; - string icon_path = Path.Combine(iconFolder, "pwIcon.ico"); - string icon_png_path = Path.Combine(iconFolder, "pwIcon.png"); - if (File.Exists(icon_png_path)) + string ico_path = Path.Combine(iconFolder, "pwIcon.ico"); + string png_path = Path.Combine(iconFolder, "pwIcon.png"); + if (File.Exists(png_path)) { - var bitmap = new System.Drawing.Bitmap(icon_png_path); // or get it from resource + var bitmap = new System.Drawing.Bitmap(png_path); IdleIcon = System.Drawing.Icon.FromHandle(bitmap.GetHicon()); } - else if (File.Exists(icon_path)) + else if (File.Exists(ico_path)) { - IdleIcon = new System.Drawing.Icon(icon_path); + IdleIcon = new System.Drawing.Icon(ico_path); } - icon_path = Path.Combine(iconFolder, "pwIconBusy.ico"); - icon_png_path = Path.Combine(iconFolder, "pwIconBusy.png"); - if (File.Exists(icon_png_path)) + ico_path = Path.Combine(iconFolder, "pwIconBusy.ico"); + png_path = Path.Combine(iconFolder, "pwIconBusy.png"); + if (File.Exists(png_path)) { - var bitmap = new System.Drawing.Bitmap(icon_png_path); + var bitmap = new System.Drawing.Bitmap(png_path); BusyIcon = System.Drawing.Icon.FromHandle(bitmap.GetHicon()); } - else if (File.Exists(icon_path)) + else if (File.Exists(ico_path)) { - BusyIcon = new System.Drawing.Icon(icon_path); + BusyIcon = new System.Drawing.Icon(ico_path); + } + + ico_path = Path.Combine(iconFolder, "pwIconUpdate.ico"); + png_path = Path.Combine(iconFolder, "pwIconUpdate.png"); + if (File.Exists(png_path)) + { + var bitmap = new System.Drawing.Bitmap(png_path); + UpdateIcon = System.Drawing.Icon.FromHandle(bitmap.GetHicon()); + } + else if (File.Exists(ico_path)) + { + UpdateIcon = new System.Drawing.Icon(ico_path); } } - systrayForm = new SystrayForm(); - systrayForm.enableUpgradeNotice = check_upgrade; + systrayForm = new SystrayForm(check_upgrade); systrayForm.autoUpgrade = auto_upgrade; - if (relaunch) + if (relaunch_delay > 0) { - Restart(2); + Restart(relaunch_delay); return; } @@ -363,10 +439,10 @@ namespace PersistentWindows.SystrayShell if (ignore_process.Length > 0) pwp.SetIgnoreProcess(ignore_process); - if (hotkey_window) + if (!File.Exists(DisableWebpageCommander) && hotkey_window) HotKeyForm.Start(hotkey); - if (!pwp.Start(auto_restore_from_db_at_startup)) + if (!pwp.Start(auto_restore_from_db_at_startup, auto_restore_last_capture_at_startup)) { systrayForm.notifyIconMain.Visible = false; return; @@ -401,17 +477,25 @@ namespace PersistentWindows.SystrayShell Log.Error("taskbar not ready, restart PersistentWindows"); } - Restart(10); + Restart(1); return false; } - static void Restart(int delay) + static public void Restart(int delay, bool hidden = true) { + Process p = new Process(); string batFile = Path.Combine(AppdataFolder, $"pw_restart.bat"); - string content = $"timeout /t {delay} /nobreak > NUL"; + string content = WaitPwFinish; + content += $"\ntimeout /t {delay} /nobreak > NUL"; content += "\nstart \"\" /B \"" + Path.Combine(Application.StartupPath, Application.ProductName) + ".exe\" " + "-wait_taskbar " + Program.CmdArgs; File.WriteAllText(batFile, content); - Process.Start(batFile); + p.StartInfo.FileName = batFile; + if (hidden) + { + p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; + p.StartInfo.UseShellExecute = true; + } + p.Start(); Log.Error("program restarted"); } @@ -426,7 +510,9 @@ namespace PersistentWindows.SystrayShell else { NotifyIcon ni = systrayForm.notifyIconMain; - ni.Icon = BusyIcon; + if (!systrayForm.toggleIcon) { + ni.Icon = BusyIcon; + } if (silent) return; @@ -451,7 +537,10 @@ namespace PersistentWindows.SystrayShell else { NotifyIcon ni = systrayForm.notifyIconMain; - ni.Icon = IdleIcon; + if (!systrayForm.toggleIcon) + { + ni.Icon = IdleIcon; + } if (Gui) { @@ -775,5 +864,14 @@ namespace PersistentWindows.SystrayShell Log.Error(format, args); } + public static void WriteDataDump() + { + pwp.WriteDataDump(); + } + + public static void Stop() + { + pwp.Stop(); + } } } diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Properties/AssemblyInfo.cs b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Properties/AssemblyInfo.cs index 442b703..03b8722 100644 --- a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Properties/AssemblyInfo.cs +++ b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.56.*")] +[assembly: AssemblyVersion("5.65.*")] diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Properties/Resources.Designer.cs b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Properties/Resources.Designer.cs index d18b9b6..f2de6c3 100644 --- a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Properties/Resources.Designer.cs +++ b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Properties/Resources.Designer.cs @@ -61,22 +61,22 @@ namespace PersistentWindows.SystrayShell.Properties { } /// - /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// Looks up a localized resource of type System.Drawing.Bitmap. /// - internal static System.Drawing.Icon pwIcon { + internal static System.Drawing.Bitmap pwIcon { get { object obj = ResourceManager.GetObject("pwIcon", resourceCulture); - return ((System.Drawing.Icon)(obj)); + return ((System.Drawing.Bitmap)(obj)); } } /// - /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// Looks up a localized resource of type System.Drawing.Bitmap. /// - internal static System.Drawing.Icon pwIcon2 { + internal static System.Drawing.Bitmap pwIcon2 { get { object obj = ResourceManager.GetObject("pwIcon2", resourceCulture); - return ((System.Drawing.Icon)(obj)); + return ((System.Drawing.Bitmap)(obj)); } } @@ -91,12 +91,12 @@ namespace PersistentWindows.SystrayShell.Properties { } /// - /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// Looks up a localized resource of type System.Drawing.Bitmap. /// - internal static System.Drawing.Icon pwIconBusy2 { + internal static System.Drawing.Bitmap pwIconBusy2 { get { object obj = ResourceManager.GetObject("pwIconBusy2", resourceCulture); - return ((System.Drawing.Icon)(obj)); + return ((System.Drawing.Bitmap)(obj)); } } @@ -110,6 +110,16 @@ namespace PersistentWindows.SystrayShell.Properties { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap pwIconUpdate2 { + get { + object obj = ResourceManager.GetObject("pwIconUpdate2", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Properties/Resources.resx b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Properties/Resources.resx index 28122dd..cd04aff 100644 --- a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Properties/Resources.resx +++ b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Properties/Resources.resx @@ -119,20 +119,23 @@ - ..\Resources\pwIcon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\pwIcon.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - ..\Resources\pwIcon2.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\pwIcon2.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\pwIconBusy.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - ..\Resources\pwIconBusy2.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\pwIconBusy2.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\pwIconUpdate.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\pwIconUpdate2.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\question.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIcon.ico b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIcon.ico index 76e5f71..6b48c59 100644 Binary files a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIcon.ico and b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIcon.ico differ diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIcon.png b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIcon.png new file mode 100644 index 0000000..bb55042 Binary files /dev/null and b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIcon.png differ diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIcon2.ico b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIcon2.ico deleted file mode 100644 index e541979..0000000 Binary files a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIcon2.ico and /dev/null differ diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIcon2.png b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIcon2.png new file mode 100644 index 0000000..e378c22 Binary files /dev/null and b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIcon2.png differ diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIconBusy2.ico b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIconBusy2.ico deleted file mode 100644 index 4383941..0000000 Binary files a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIconBusy2.ico and /dev/null differ diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIconBusy2.png b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIconBusy2.png new file mode 100644 index 0000000..f3ad2ce Binary files /dev/null and b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIconBusy2.png differ diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIconUpdate.png b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIconUpdate.png index 4a16685..2635303 100644 Binary files a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIconUpdate.png and b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIconUpdate.png differ diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIconUpdate2.png b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIconUpdate2.png new file mode 100644 index 0000000..94b255c Binary files /dev/null and b/Ninjacrab.PersistentWindows.Solution/SystrayShell/Resources/pwIconUpdate2.png differ diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/SplashForm.resx b/Ninjacrab.PersistentWindows.Solution/SystrayShell/SplashForm.resx deleted file mode 100644 index 1f666f2..0000000 --- a/Ninjacrab.PersistentWindows.Solution/SystrayShell/SplashForm.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 17, 17 - - \ No newline at end of file diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayForm.Designer.cs b/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayForm.Designer.cs index 575e4f9..a2ebe39 100644 --- a/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayForm.Designer.cs +++ b/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayForm.Designer.cs @@ -168,10 +168,6 @@ namespace PersistentWindows.SystrayShell // pause/resume upgrade notice //this.upgradeNoticeMenuItem.Text = "Disable upgrade notice"; this.upgradeNoticeMenuItem.Click += new System.EventHandler(this.PauseResumeUpgradeNotice); - if (this.enableUpgradeNotice) - this.upgradeNoticeMenuItem.Text = "Disable upgrade notice"; - else - this.upgradeNoticeMenuItem.Text = "Enable upgrade notice"; // // exitToolStripMenuItem diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayForm.cs b/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayForm.cs index 6f62633..28c2a48 100644 --- a/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayForm.cs +++ b/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayForm.cs @@ -17,16 +17,11 @@ namespace PersistentWindows.SystrayShell { private const int MaxSnapshots = 38; // 0-9, a-z, ` and final one for undo - public bool restoreToolStripMenuItemEnabled; - public bool restoreSnapshotMenuItemEnabled; - private bool pauseAutoRestore = false; - private bool toggleIcon = false; + public bool toggleIcon = false; - public bool enableUpgradeNotice = true; private int skipUpgradeCounter = 0; private bool pauseUpgradeCounter = false; - private bool foundUpgrade = false; public bool autoUpgrade = false; @@ -43,10 +38,25 @@ namespace PersistentWindows.SystrayShell private Dictionary upgradeDownloaded = new Dictionary(); - public SystrayForm() + public SystrayForm(bool enable_upgrade_notice) { InitializeComponent(); + if (File.Exists(Program.DisableUpgradeNotice)) + upgradeNoticeMenuItem.Text = "Enable upgrade notice"; + else if (!enable_upgrade_notice) + { + File.Create(Program.DisableUpgradeNotice); + upgradeNoticeMenuItem.Text = "Enable upgrade notice"; + } + else + upgradeNoticeMenuItem.Text = "Disable upgrade notice"; + + if (File.Exists(Program.DisableWebpageCommander)) + { + invokeWebCommander.Text = "Enable webpage commander"; + } + clickDelayTimer = new System.Timers.Timer(1000); clickDelayTimer.Elapsed += ClickTimerCallBack; clickDelayTimer.SynchronizingObject = this.contextMenuStripSysTray; @@ -166,7 +176,6 @@ namespace PersistentWindows.SystrayShell ctrlKeyPressed = 0; shiftKeyPressed = 0; altKeyPressed = 0; - } //private void TimerEventProcessor(Object myObject, EventArgs myEventArgs) @@ -177,7 +186,7 @@ namespace PersistentWindows.SystrayShell else restoreToolStripMenuItem.Image = Properties.Resources.question; - if (checkUpgrade && enableUpgradeNotice) + if (checkUpgrade && upgradeNoticeMenuItem.Text.Contains("Disable")) { if (pauseUpgradeCounter) { @@ -239,10 +248,21 @@ namespace PersistentWindows.SystrayShell || current_major == latest_major && current_minor < latest_minor) { notifyIconMain.ShowBalloonTip(5000, $"{Application.ProductName} {latestVersion} upgrade is available", "The upgrade notice can be disabled in menu", ToolTipIcon.Info); - foundUpgrade = true; + upgradeNoticeMenuItem.Text = $"Upgrade to {latestVersion}"; if (!upgradeDownloaded.ContainsKey(latestVersion)) { + string url = Program.ProjectUrl + "/releases"; + var os_version = Environment.OSVersion; + if (os_version.Version.Major < 10) + Process.Start(url); + else if (os_version.Version.Build < 22000) + Process.Start(url); + /* windows 11 + else + Process.Start(new ProcessStartInfo(url)); + */ + var src_file = $"{Program.ProjectUrl}/releases/download/{latestVersion}/{System.Windows.Forms.Application.ProductName}{latestVersion}.zip"; var dst_file = $"{Program.AppdataFolder}/upgrade.zip"; var dst_dir = Path.Combine($"{Program.AppdataFolder}", "upgrade"); @@ -256,7 +276,7 @@ namespace PersistentWindows.SystrayShell upgradeDownloaded[latestVersion] = true; string batFile = Path.Combine(Program.AppdataFolder, $"pw_upgrade.bat"); - string content = "timeout /t 5 /nobreak > NUL"; + string content = Program.WaitPwFinish; content += $"\ncopy /Y \"{dst_dir}\\*.*\" \"{install_dir}\""; content += "\nstart \"\" /B \"" + Path.Combine(install_dir, Application.ProductName) + ".exe\" " + Program.CmdArgs; File.WriteAllText(batFile, content); @@ -264,10 +284,7 @@ namespace PersistentWindows.SystrayShell if (autoUpgrade) Upgrade(); else - { - upgradeNoticeMenuItem.Text = $"Upgrade to {latestVersion}"; notifyIconMain.Icon = Program.UpdateIcon; - } } } } @@ -275,15 +292,24 @@ namespace PersistentWindows.SystrayShell private void Exit() { -#if DEBUG + var process = Process.GetCurrentProcess(); + process.PriorityClass = ProcessPriorityClass.High; + + Program.WriteDataDump(); + Log.Event("Session exit"); + this.notifyIconMain.Visible = false; -#endif //this.notifyIconMain.Icon = null; + Log.Exit(); + Program.Stop(); Application.Exit(); } + private void Upgrade() { + Program.WriteDataDump(); + string batFile = Path.Combine(Program.AppdataFolder, "pw_upgrade.bat"); Process.Start(batFile); Exit(); @@ -345,11 +371,21 @@ namespace PersistentWindows.SystrayShell HotKeyForm.InvokeFromMenu(); else if (this.invokeWebCommander.Text.Contains("Disable")) { + File.Create(Program.DisableWebpageCommander); this.invokeWebCommander.Text = "Enable webpage commander"; HotKeyForm.Stop(); } else { + try + { + File.Delete(Program.DisableWebpageCommander); + } + catch (Exception ex) + { + Log.Error(ex.ToString()); + } + this.invokeWebCommander.Text = "Disable webpage commander"; HotKeyForm.Start(Program.hotkey); } @@ -394,20 +430,27 @@ namespace PersistentWindows.SystrayShell private void PauseResumeUpgradeNotice(Object sender, EventArgs e) { - if (foundUpgrade) + if (upgradeNoticeMenuItem.Text.Contains("Upgrade to")) { Upgrade(); } - else if (enableUpgradeNotice) + else if (upgradeNoticeMenuItem.Text.Contains("Enable")) { - enableUpgradeNotice = false; - upgradeNoticeMenuItem.Text = "Enable upgrade notice"; - } - else - { - enableUpgradeNotice = true; upgradeNoticeMenuItem.Text = "Disable upgrade notice"; CheckUpgradeSafe(); + try + { + File.Delete(Program.DisableUpgradeNotice); + } + catch (Exception ex) + { + Log.Error(ex.ToString()); + } + } + else //menu is "Disable upgrade notice" + { + File.Create(Program.DisableUpgradeNotice); + upgradeNoticeMenuItem.Text = "Enable upgrade notice"; } } @@ -418,6 +461,9 @@ namespace PersistentWindows.SystrayShell private void ExitToolStripMenuItemClickHandler(object sender, EventArgs e) { + bool ctrl_key_pressed = (User32.GetKeyState(0x11) & 0x8000) != 0; + if (ctrl_key_pressed) + Program.Restart(2, hidden:false); Exit(); } diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayForm.resx b/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayForm.resx deleted file mode 100644 index 68a1d6d..0000000 --- a/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayForm.resx +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 17, 17 - - - 150, 17 - - - \ No newline at end of file diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayShell.csproj b/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayShell.csproj index 662521e..d4db022 100644 --- a/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayShell.csproj +++ b/Ninjacrab.PersistentWindows.Solution/SystrayShell/SystrayShell.csproj @@ -52,7 +52,9 @@ - + + app.manifest + @@ -84,19 +86,12 @@ - - SplashForm.cs - Designer - - - SystrayForm.cs - Designer - ResXFileCodeGenerator Designer Resources.Designer.cs + SettingsSingleFileGenerator Settings.Designer.cs @@ -131,12 +126,6 @@ - - - - - - copy $(SolutionDir)*.bat $(TargetDir) diff --git a/Ninjacrab.PersistentWindows.Solution/SystrayShell/app.manifest b/Ninjacrab.PersistentWindows.Solution/SystrayShell/app.manifest index 39e2b4e..a39556b 100644 --- a/Ninjacrab.PersistentWindows.Solution/SystrayShell/app.manifest +++ b/Ninjacrab.PersistentWindows.Solution/SystrayShell/app.manifest @@ -28,16 +28,16 @@ and Windows will automatically select the most compatible environment. --> - + - + - + - + diff --git a/README.md b/README.md index 0587d09..4b84fd4 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,11 @@ and restores back to its previous settings. this tool and not have to worry about re-arranging when all is back to normal. ## Key Features -- Keeps track of window position changes, and automatically restores the desktop layout, including the taskbar position, to the last matching monitor setup. +- Auto restore: Keeps track of window position changes, and automatically restores the desktop layout, including the taskbar position, to the last matching monitor setup. - Supports remote desktop sessions with multiple display configurations. -- Capture windows to disk: saves desktop layout capture to hard drive in liteDB format, so that closed windows can be restored after PC reboot, with virtual desktop observed. -- Capture snapshot to ram: saves desktop layout in memory using one char from [0-9a-z] as the name. The window Z-order is preserved in the snapshot. +- Capture windows to disk: manually saves desktop layout capture to hard drive in liteDB format, so that closed windows can be restored to corresponding virtual desktop after PC reboot. +- Capture snapshot: manually saves desktop layout to ram. The window Z-order is preserved in the snapshot. Up to 36 snapshots ([0-9a-z]) can be taken for each display configuration. +- Automatically persists the location history of all windows (alive and closed) to hard drive in xml format, so that manual-restore-point (aka snapshot) and auto-restore-point will continue to function smoothly upon app upgrade/restart, even after PC reboot. - Webpage commander to improve the efficiency of web browsing for all major web browsers using one-letter commands like in vi editor. - Efficient window switching between foreground and background dual positions. - Pause/resume auto restore. @@ -28,11 +29,11 @@ this tool and not have to worry about re-arranging when all is back to normal. > Note: the program can be run from any directory, but the program saves its data in > *C:\Users\\[User]\AppData\Local\PersistentWindows* +**For PersistentWindows to be able to restore windows with elevated privileges (for tools like Task Manager or Event Viewer), it needs to be run with Administrator privileges.** + ### To set up PersistentWindows to automatically start at user login: This can be done by creating a task in **Task Scheduler**, or by adding a shortcut to the **Startup Folder** (shell:startup). -For PersistentWindows to be able to restore windows with elevated privileges (for tools like Task Manager or Event Viewer), it needs to be run with Administrator privileges. - Choose **one** of the three options: **Task Scheduler (Windows 10/11)** diff --git a/webpage_commander.md b/webpage_commander.md new file mode 100644 index 0000000..d1234ae --- /dev/null +++ b/webpage_commander.md @@ -0,0 +1,5 @@ + +### Webpage commander window is invoked via hotkey (Alt + W) +* If the invocation is unintentional, press the hotkey again to revoke webpage commander. The hotkey can be disabled via PW menu or command line. +* Webpage commander improves the efficiency of web browsing using single-letter command shortcut. +* Check out [Online Help](https://www.github.com/kangyu-california/PersistentWindows/blob/master/Help.md) for detailed instructions. \ No newline at end of file