diff --git a/src/ManagedShell.AppBar/FullScreenApp.cs b/src/ManagedShell.AppBar/FullScreenApp.cs index 6d9e89f5..6171c454 100644 --- a/src/ManagedShell.AppBar/FullScreenApp.cs +++ b/src/ManagedShell.AppBar/FullScreenApp.cs @@ -1,5 +1,4 @@ using System; -using ManagedShell.Interop; namespace ManagedShell.AppBar { @@ -7,7 +6,7 @@ public class FullScreenApp { public IntPtr hWnd; public ScreenInfo screen; - public NativeMethods.Rect rect; public string title; + public bool fromTasksService; } } \ No newline at end of file diff --git a/src/ManagedShell.AppBar/FullScreenHelper.cs b/src/ManagedShell.AppBar/FullScreenHelper.cs index 05ad6c08..6d13434b 100644 --- a/src/ManagedShell.AppBar/FullScreenHelper.cs +++ b/src/ManagedShell.AppBar/FullScreenHelper.cs @@ -17,6 +17,7 @@ public sealed class FullScreenHelper : IDisposable private readonly TasksService _tasksService; public ObservableCollection FullScreenApps = new ObservableCollection(); + public ObservableCollection InactiveFullScreenApps = new ObservableCollection(); public FullScreenHelper(TasksService tasksService) { @@ -25,8 +26,7 @@ public FullScreenHelper(TasksService tasksService) if (_tasksService != null && EnvironmentHelper.IsWindows8OrBetter) { // On Windows 8 and newer, TasksService will tell us when windows enter and exit full screen - _tasksService.FullScreenEntered += TasksService_Event; - _tasksService.FullScreenLeft += TasksService_Event; + _tasksService.FullScreenChanged += TasksService_FullScreenChanged; _tasksService.MonitorChanged += TasksService_Event; _tasksService.DesktopActivated += TasksService_Event; _tasksService.WindowActivated += TasksService_Event; @@ -47,6 +47,53 @@ private void TasksService_Event(object sender, EventArgs e) updateFullScreenWindows(); } + private void TasksService_FullScreenChanged(object sender, FullScreenEventArgs e) + { + if (FullScreenApps.Any(app => app.hWnd == e.Handle) == e.IsEntering) + { + if (e.IsEntering) + { + FullScreenApp existingApp = FullScreenApps.First(app => app.hWnd == e.Handle); + if (!existingApp.fromTasksService) + { + // Grant this app TasksService treatment + existingApp.fromTasksService = true; + } + } + return; + } + + if (InactiveFullScreenApps.Count > 0 && InactiveFullScreenApps.Any(app => app.hWnd == e.Handle)) + { + // If this window is in the inactive list, remove it because it is no longer needed + InactiveFullScreenApps.Remove(InactiveFullScreenApps.First(app => app.hWnd == e.Handle)); + } + + if (e.IsEntering) + { + // When TasksService gives us a full-screen window handle, trust that it is full-screen in terms of bounds + FullScreenApp appNew = getFullScreenApp(e.Handle, true); + + if (appNew != null) + { + ShellLogger.Debug($"FullScreenHelper: Adding full screen app from TasksService {appNew.hWnd} ({appNew.title})"); + FullScreenApps.Add(appNew); + } + } + else + { + foreach (FullScreenApp app in FullScreenApps) + { + if (app.hWnd == e.Handle) + { + ShellLogger.Debug($"FullScreenHelper: Removing full screen app from TasksService {app.hWnd} ({app.title})"); + FullScreenApps.Remove(app); + break; + } + } + } + } + private void FullscreenCheck_Tick(object sender, EventArgs e) { updateFullScreenWindows(); @@ -57,27 +104,51 @@ private void updateFullScreenWindows() IntPtr hWnd = GetForegroundWindow(); List removeApps = new List(); + List removeInactiveApps = new List(); bool skipAdd = false; // first check if this window is already in our list. if so, remove it if necessary foreach (FullScreenApp app in FullScreenApps) { - FullScreenApp appCurrentState = getFullScreenApp(app.hWnd); - - if (app.hWnd == hWnd && appCurrentState != null && app.screen.DeviceName == appCurrentState.screen.DeviceName) - { - // this window, still same screen, do nothing - skipAdd = true; - continue; - } + FullScreenApp appCurrentState = getFullScreenApp(app.hWnd, app.fromTasksService); - if (appCurrentState != null && app.hWnd != hWnd && - app.screen.DeviceName == appCurrentState.screen.DeviceName && - Screen.FromHandle(hWnd).DeviceName != appCurrentState.screen.DeviceName) + if (appCurrentState != null) { - // if the full-screen window is no longer foreground, keep it - // as long as the foreground window is on a different screen. - continue; + // App is still full-screen + if (app.hWnd == hWnd) + { + // App is still foreground + if (app.screen.DeviceName != appCurrentState.screen.DeviceName) + { + // The app moved to another monitor + // Remove and add back to collection to trigger change events + // This will be added back immediately because it is foreground + ShellLogger.Debug($"FullScreenHelper: Monitor changed for full screen app {app.hWnd} ({app.title})"); + } + else + { + // Still same screen, do nothing + skipAdd = true; + continue; + } + } + else if (Screen.FromHandle(hWnd).DeviceName != appCurrentState.screen.DeviceName) + { + // if the full-screen window is no longer foreground, keep it + // as long as the foreground window is on a different screen. + continue; + } + else + { + // Still full screen but no longer active + if ((GetWindowLong(hWnd, GWL_EXSTYLE) & (int)ExtendedWindowStyles.WS_EX_TOPMOST) == (int)ExtendedWindowStyles.WS_EX_TOPMOST) + { + // If the new foreground window is a topmost window, don't consider this full-screen app inactive + continue; + } + ShellLogger.Debug($"FullScreenHelper: Inactive full screen app {app.hWnd} ({app.title})"); + } + InactiveFullScreenApps.Add(app); } removeApps.Add(app); @@ -93,24 +164,113 @@ private void updateFullScreenWindows() } } + // clean up any inactive windows that are no longer full-screen + if (InactiveFullScreenApps.Count > 0) + { + foreach (FullScreenApp app in InactiveFullScreenApps) + { + FullScreenApp appCurrentState = getFullScreenApp(app.hWnd, app.fromTasksService); + if (appCurrentState == null) + { + // No longer a full-screen window + removeInactiveApps.Add(app); + } + else if (appCurrentState.screen.DeviceName != app.screen.DeviceName) + { + // The app moved to another monitor while inactive + app.screen = appCurrentState.screen; + } + } + } + + // remove any changed inactive windows we found + if (removeInactiveApps.Count > 0) + { + foreach (FullScreenApp existingApp in removeInactiveApps) + { + ShellLogger.Debug($"FullScreenHelper: Removing inactive full screen app {existingApp.hWnd} ({existingApp.title})"); + InactiveFullScreenApps.Remove(existingApp); + } + } + // check if this is a new full screen app if (!skipAdd) { - FullScreenApp appNew = getFullScreenApp(hWnd); - if (appNew != null) + FullScreenApp appAdd; + bool wasInactive = false; + if (InactiveFullScreenApps.Count > 0 && InactiveFullScreenApps.Any(app => app.hWnd == hWnd)) { - ShellLogger.Debug($"FullScreenHelper: Adding full screen app {appNew.hWnd} ({appNew.title})"); - FullScreenApps.Add(appNew); + // This is a previously-active full-screen app that became active again. + wasInactive = true; + appAdd = InactiveFullScreenApps.First(app => app.hWnd == hWnd); + } + else + { + appAdd = getFullScreenApp(hWnd); + } + + if (appAdd != null) + { + ShellLogger.Debug($"FullScreenHelper: Adding{(wasInactive ? " reactivated" : "")} full screen app {appAdd.hWnd} ({appAdd.title})"); + FullScreenApps.Add(appAdd); + if (wasInactive) + { + InactiveFullScreenApps.Remove(appAdd); + } } } } - private FullScreenApp getFullScreenApp(IntPtr hWnd) + private FullScreenApp getFullScreenApp(IntPtr hWnd, bool fromTasksService = false) + { + ScreenInfo screenInfo = null; + + if (!fromTasksService) + { + Rect rect = GetEffectiveWindowRect(hWnd); + var allScreens = Screen.AllScreens.Select(ScreenInfo.Create).ToList(); + if (allScreens.Count > 1) allScreens.Add(ScreenInfo.CreateVirtualScreen()); + + foreach (var screen in allScreens) + { + if (rect.Top == screen.Bounds.Top && rect.Left == screen.Bounds.Left && + rect.Bottom == screen.Bounds.Bottom && rect.Right == screen.Bounds.Right) + { + screenInfo = screen; + break; + } + } + + if (screenInfo == null) + { + // If the window rect does not match any screen's bounds, it's not full screen + return null; + } + } + + ApplicationWindow win = new ApplicationWindow(null, hWnd); + if (!CanFullScreen(win)) + { + return null; + } + + if (screenInfo == null) + { + screenInfo = ScreenInfo.Create(Screen.FromHandle(hWnd)); + } + + // this is a full screen app + return new FullScreenApp { hWnd = hWnd, screen = screenInfo, title = win.Title, fromTasksService = fromTasksService }; + } + + private Rect GetEffectiveWindowRect(IntPtr hWnd) { int style = GetWindowLong(hWnd, GWL_STYLE); Rect rect; - if ((((int)WindowStyles.WS_CAPTION | (int)WindowStyles.WS_THICKFRAME) & style) == ((int)WindowStyles.WS_CAPTION | (int)WindowStyles.WS_THICKFRAME)) + if ((((int)WindowStyles.WS_CAPTION | (int)WindowStyles.WS_THICKFRAME) & style) == ((int)WindowStyles.WS_CAPTION | (int)WindowStyles.WS_THICKFRAME) || + (((uint)WindowStyles.WS_POPUP | (uint)WindowStyles.WS_THICKFRAME) & style) == ((uint)WindowStyles.WS_POPUP | (uint)WindowStyles.WS_THICKFRAME) || + (((uint)WindowStyles.WS_POPUP | (uint)WindowStyles.WS_BORDER) & style) == ((uint)WindowStyles.WS_POPUP | (uint)WindowStyles.WS_BORDER)) { GetClientRect(hWnd, out rect); MapWindowPoints(hWnd, IntPtr.Zero, ref rect, 2); @@ -120,70 +280,66 @@ private FullScreenApp getFullScreenApp(IntPtr hWnd) GetWindowRect(hWnd, out rect); } - var allScreens = Screen.AllScreens.Select(ScreenInfo.Create).ToList(); - if (allScreens.Count > 1) allScreens.Add(ScreenInfo.CreateVirtualScreen()); + return rect; + } - // check if this is a fullscreen app - foreach (var screen in allScreens) + private bool CanFullScreen(ApplicationWindow window) + { + // make sure this is not us + GetWindowThreadProcessId(window.Handle, out uint hwndProcId); + if (hwndProcId == GetCurrentProcessId()) { - if (rect.Top == screen.Bounds.Top && rect.Left == screen.Bounds.Left && - rect.Bottom == screen.Bounds.Bottom && rect.Right == screen.Bounds.Right) - { - // make sure this is not us - GetWindowThreadProcessId(hWnd, out uint hwndProcId); - if (hwndProcId == GetCurrentProcessId()) - { - return null; - } - - // make sure this is fullscreen-able - if (!IsWindow(hWnd) || !IsWindowVisible(hWnd) || IsIconic(hWnd)) - { - return null; - } + return false; + } - // Make sure this isn't explicitly marked as being non-rude - IntPtr isNonRudeHwnd = GetProp(hWnd, "NonRudeHWND"); - if (isNonRudeHwnd != IntPtr.Zero) - { - return null; - } + // make sure this is fullscreen-able + if (!IsWindow(window.Handle) || !IsWindowVisible(window.Handle) || IsIconic(window.Handle)) + { + return false; + } - // make sure this is not a cloaked window - if (EnvironmentHelper.IsWindows8OrBetter) - { - int cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(uint)); - DwmGetWindowAttribute(hWnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAKED, out uint cloaked, cbSize); - if (cloaked > 0) - { - return null; - } - } + // Make sure this isn't explicitly marked as being non-rude + IntPtr isNonRudeHwnd = GetProp(window.Handle, "NonRudeHWND"); + if (isNonRudeHwnd != IntPtr.Zero) + { + return false; + } - ApplicationWindow win = new ApplicationWindow(null, hWnd); - if (!EnvironmentHelper.IsWindows8OrBetter) - { - // make sure this is not the shell desktop - // In Windows 8 and newer, the NonRudeHWND property is set and this is not needed - if (win.ClassName == "Progman" || win.ClassName == "WorkerW") - { - return null; - } - } + // make sure this is not a cloaked window + if (EnvironmentHelper.IsWindows8OrBetter) + { + int cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(uint)); + DwmGetWindowAttribute(window.Handle, DWMWINDOWATTRIBUTE.DWMWA_CLOAKED, out uint cloaked, cbSize); + if (cloaked > 0) + { + return false; + } + } - // make sure this is not a transparent window - int styles = win.ExtendedWindowStyles; - if ((styles & (int)ExtendedWindowStyles.WS_EX_LAYERED) != 0 && ((styles & (int)ExtendedWindowStyles.WS_EX_TRANSPARENT) != 0 || (styles & (int)ExtendedWindowStyles.WS_EX_NOACTIVATE) != 0)) - { - return null; - } + // make sure this is not an immersive shell window + if (window.IsImmersiveShellWindow()) + { + return false; + } - // this is a full screen app on this screen - return new FullScreenApp { hWnd = hWnd, screen = screen, rect = rect, title = win.Title }; + if (!EnvironmentHelper.IsWindows8OrBetter) + { + // make sure this is not the shell desktop + // In Windows 8 and newer, the NonRudeHWND property is set and this is not needed + if (window.ClassName == "Progman" || window.ClassName == "WorkerW") + { + return false; } } - return null; + // make sure this is not a transparent window + int styles = window.ExtendedWindowStyles; + if ((styles & (int)ExtendedWindowStyles.WS_EX_LAYERED) != 0 && ((styles & (int)ExtendedWindowStyles.WS_EX_TRANSPARENT) != 0 || (styles & (int)ExtendedWindowStyles.WS_EX_NOACTIVATE) != 0)) + { + return false; + } + + return true; } private void ResetScreenCache() @@ -212,8 +368,7 @@ public void Dispose() if (_tasksService != null && EnvironmentHelper.IsWindows8OrBetter) { - _tasksService.FullScreenEntered -= TasksService_Event; - _tasksService.FullScreenLeft -= TasksService_Event; + _tasksService.FullScreenChanged -= TasksService_FullScreenChanged; _tasksService.MonitorChanged -= TasksService_Event; _tasksService.DesktopActivated -= TasksService_Event; _tasksService.WindowActivated -= TasksService_Event; diff --git a/src/ManagedShell.WindowsTasks/ApplicationWindow.cs b/src/ManagedShell.WindowsTasks/ApplicationWindow.cs index 03411df5..219d3320 100644 --- a/src/ManagedShell.WindowsTasks/ApplicationWindow.cs +++ b/src/ManagedShell.WindowsTasks/ApplicationWindow.cs @@ -411,22 +411,14 @@ private bool getShowInTaskbar() if (cloaked > 0) { - ShellLogger.Debug($"ApplicationWindow: Cloaked ({cloaked}) window ({Title}) hidden from taskbar"); + ShellLogger.Debug($"ApplicationWindow: Cloaked window {Handle} ({Title}) hidden from taskbar"); return false; } // UWP shell windows that are not cloaked should be hidden from the taskbar, too. - if (ClassName == "ApplicationFrameWindow" || ClassName == "Windows.UI.Core.CoreWindow" || ClassName == "StartMenuSizingFrame") + if (IsImmersiveShellWindow()) { - if ((ExtendedWindowStyles & (int)NativeMethods.ExtendedWindowStyles.WS_EX_WINDOWEDGE) == 0) - { - ShellLogger.Debug($"ApplicationWindow: Hiding UWP non-window {Title}"); - return false; - } - } - else if (!EnvironmentHelper.IsWindows10OrBetter && (ClassName == "ImmersiveBackgroundWindow" || ClassName == "SearchPane" || ClassName == "NativeHWNDHost" || ClassName == "Shell_CharmWindow" || ClassName == "ImmersiveLauncher") && WinFileName.ToLower().Contains("explorer.exe")) - { - ShellLogger.Debug($"ApplicationWindow: Hiding immersive shell window {Title}"); + ShellLogger.Debug($"ApplicationWindow: Hiding immersive shell window {Handle} ({Title}) from taskbar"); return false; } } @@ -434,6 +426,28 @@ private bool getShowInTaskbar() return CanAddToTaskbar; } + public bool IsImmersiveShellWindow() + { + if (!EnvironmentHelper.IsWindows8OrBetter) + { + return false; + } + + if (ClassName == "ApplicationFrameWindow" || ClassName == "Windows.UI.Core.CoreWindow" || ClassName == "StartMenuSizingFrame" || ClassName == "Shell_LightDismissOverlay") + { + if ((ExtendedWindowStyles & (int)NativeMethods.ExtendedWindowStyles.WS_EX_WINDOWEDGE) == 0) + { + return true; + } + } + else if (!EnvironmentHelper.IsWindows10OrBetter && (ClassName == "ImmersiveBackgroundWindow" || ClassName == "SearchPane" || ClassName == "NativeHWNDHost" || ClassName == "Shell_CharmWindow" || ClassName == "ImmersiveLauncher") && WinFileName.ToLower().Contains("explorer.exe")) + { + return true; + } + + return false; + } + private string getFileDescription() { string desc; diff --git a/src/ManagedShell.WindowsTasks/FullScreenEventArgs.cs b/src/ManagedShell.WindowsTasks/FullScreenEventArgs.cs new file mode 100644 index 00000000..cd27d63c --- /dev/null +++ b/src/ManagedShell.WindowsTasks/FullScreenEventArgs.cs @@ -0,0 +1,10 @@ +using System; + +namespace ManagedShell.WindowsTasks +{ + public class FullScreenEventArgs : EventArgs + { + public IntPtr Handle; + public bool IsEntering; + } +} diff --git a/src/ManagedShell.WindowsTasks/TasksService.cs b/src/ManagedShell.WindowsTasks/TasksService.cs index 6a85d3f3..331cae5b 100644 --- a/src/ManagedShell.WindowsTasks/TasksService.cs +++ b/src/ManagedShell.WindowsTasks/TasksService.cs @@ -20,8 +20,7 @@ public class TasksService : DependencyObject, IDisposable public event EventHandler WindowActivated; public event EventHandler DesktopActivated; - public event EventHandler FullScreenEntered; - public event EventHandler FullScreenLeft; + public event EventHandler FullScreenChanged; public event EventHandler MonitorChanged; private NativeWindowEx _HookWin; @@ -462,12 +461,30 @@ private void ShellWinProc(ref Message msg, ref bool handled) break; case HSHELL.FULLSCREENENTER: - FullScreenEntered?.Invoke(this, new EventArgs()); - break; + { + FullScreenEventArgs args = new FullScreenEventArgs + { + Handle = msgCopy.LParam, + IsEntering = true + }; + + FullScreenChanged?.Invoke(this, args); + ShellLogger.Debug($"TasksService: Full screen entered by window {msgCopy.LParam}"); + break; + } case HSHELL.FULLSCREENEXIT: - FullScreenLeft?.Invoke(this, new EventArgs()); - break; + { + FullScreenEventArgs args = new FullScreenEventArgs + { + Handle = msgCopy.LParam, + IsEntering = false + }; + + FullScreenChanged?.Invoke(this, args); + ShellLogger.Debug($"TasksService: Full screen exited by window {msgCopy.LParam}"); + break; + } case HSHELL.GETMINRECT: SHELLHOOKINFO minRectInfo = Marshal.PtrToStructure(msg.LParam); @@ -526,14 +543,13 @@ private void ShellWinProc(ref Message msg, ref bool handled) // MarkFullscreenWindow // Also sends WM_SHELLHOOK message ShellLogger.Debug("TasksService: ITaskbarList: MarkFullscreenWindow HWND:" + msg.LParam + " Entering? " + msg.WParam); - if (msg.WParam == IntPtr.Zero) + FullScreenEventArgs args = new FullScreenEventArgs { - FullScreenLeft?.Invoke(this, new EventArgs()); - } - else - { - FullScreenEntered?.Invoke(this, new EventArgs()); - } + Handle = msgCopy.LParam, + IsEntering = msg.WParam != IntPtr.Zero + }; + + FullScreenChanged?.Invoke(this, args); msg.Result = IntPtr.Zero; return; case (int)WM.USER + 64: