Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/COM/PowerSetting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ public struct POWERBROADCAST_SETTING

internal class PowerSetting
{
private static readonly Guid GUID_CONSOLE_DISPLAY_STATE =
new Guid("6fe69556-704a-47a0-8f24-c28d936fda47");
// Interactive applications monitor the display associated with their own session.
// https://learn.microsoft.com/windows/win32/power/power-setting-guids
private static readonly Guid GUID_SESSION_DISPLAY_STATUS =
new Guid("2b84c20e-ad23-4ddf-93db-05ffbd7efca5");

private static IntPtr hPowerNotify;
private static int? lastDisplayState = null;
Expand All @@ -33,7 +35,7 @@ private static extern IntPtr RegisterPowerSettingNotification(

public static void RegisterWindow(IntPtr windowHandle)
{
Guid guid = GUID_CONSOLE_DISPLAY_STATE;
Guid guid = GUID_SESSION_DISPLAY_STATUS;
hPowerNotify = RegisterPowerSettingNotification(windowHandle, ref guid, 0);

if (hPowerNotify != IntPtr.Zero)
Expand All @@ -53,15 +55,16 @@ public static bool IsExitingStandby(nint msgPtr)
{
var powerSetting = Marshal.PtrToStructure<POWERBROADCAST_SETTING>(msgPtr);

if (powerSetting.PowerSetting.Equals(GUID_CONSOLE_DISPLAY_STATE))
if (powerSetting.PowerSetting.Equals(GUID_SESSION_DISPLAY_STATUS) &&
powerSetting.DataLength == sizeof(uint))
{
IntPtr dataPtr = new(msgPtr.ToInt64() + Marshal.SizeOf<POWERBROADCAST_SETTING>());
int displayState = Marshal.ReadInt32(dataPtr);

bool stateChanged = lastDisplayState.HasValue && lastDisplayState.Value != displayState;
lastDisplayState = displayState;

// If state changed and display is turning on, assume we are exiting standby
// Display-on also catches ordinary monitor wake and undimming; checking the schedule is harmless.
return stateChanged && displayState == 1;
}

Expand Down
156 changes: 117 additions & 39 deletions src/EventScheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows.Forms;

namespace WinDynamicDesktop
Expand All @@ -26,16 +27,33 @@ public class DisplayEvent

class EventScheduler
{
private System.Timers.Timer backgroundTimer = new System.Timers.Timer();
private System.Timers.Timer schedulerTimer = new System.Timers.Timer();
private readonly object schedulerLock = new object();
private readonly ISchedulerTimer backgroundTimer;
private readonly ISchedulerTimer schedulerTimer;
private readonly Func<int> getDisplayCount;
private readonly Func<PlatformHelper> getPlatformHelper;
private const long timerError = (long)(TimeSpan.TicksPerMillisecond * 15.6);

public FullScreenApi fullScreenChecker;
private List<DisplayEvent> displayEvents;
private DateTime? nextUpdateTime;

public EventScheduler()
public EventScheduler() : this(new SchedulerTimer(), new SchedulerTimer(),
() => Screen.AllScreens.Length, UwpDesktop.GetHelper)
{
SystemEvents.DisplaySettingsChanged += OnDisplaySettingsChanged;
SystemEvents.PowerModeChanged += OnPowerModeChanged;
SystemEvents.SessionSwitch += OnSessionSwitch;
SystemEvents.TimeChanged += OnTimeChanged;
}

internal EventScheduler(ISchedulerTimer backgroundTimer, ISchedulerTimer schedulerTimer,
Func<int> getDisplayCount, Func<PlatformHelper> getPlatformHelper)
{
this.backgroundTimer = backgroundTimer;
this.schedulerTimer = schedulerTimer;
this.getDisplayCount = getDisplayCount;
this.getPlatformHelper = getPlatformHelper;
fullScreenChecker = new FullScreenApi(() => HandleTimerEvent(true));

backgroundTimer.AutoReset = true;
Expand All @@ -44,13 +62,18 @@ public EventScheduler()
backgroundTimer.Start();

schedulerTimer.Elapsed += OnSchedulerTimerElapsed;
SystemEvents.DisplaySettingsChanged += OnDisplaySettingsChanged;
SystemEvents.PowerModeChanged += OnPowerModeChanged;
SystemEvents.SessionSwitch += OnSessionSwitch;
SystemEvents.TimeChanged += OnTimeChanged;
}

public bool Run(bool forceImageUpdate = false, DisplayEvent overrideEvent = null)
{
// Timer, IPC, and UI updates must keep wallpaper writes and their cached paths in the same order.
lock (schedulerLock)
{
return RunCore(forceImageUpdate, overrideEvent);
}
}

private bool RunCore(bool forceImageUpdate, DisplayEvent overrideEvent)
{
if (!LaunchSequence.IsLocationReady() || !LaunchSequence.IsThemeReady())
{
Expand All @@ -65,7 +88,8 @@ public bool Run(bool forceImageUpdate = false, DisplayEvent overrideEvent = null
forceImageUpdate = UpdateDisplayList() || forceImageUpdate;
SolarData data = SunriseSunsetService.GetSolarData(DateTime.Today);
LoggingHandler.LogMessage("Calculated solar data: {0}", data);
long nextDisplayUpdateTicks = ThemeShuffler.MaybeShuffleWallpaper(data)?.Ticks ?? long.MaxValue;
long nextDisplayUpdateTicks = ThemeShuffler.MaybeShuffleWallpaper()?.Ticks ?? long.MaxValue;
string[] activeThemes = JsonConfig.settings.activeThemes;

for (int i = 0; i < displayEvents.Count; i++)
{
Expand All @@ -80,19 +104,18 @@ public bool Run(bool forceImageUpdate = false, DisplayEvent overrideEvent = null

if (i < displayEvents.Count - 1)
{
string themeId = JsonConfig.settings.activeThemes[0];
if (themeId == null && JsonConfig.settings.activeThemes.Length > 1 &&
i + 1 < JsonConfig.settings.activeThemes.Length)
string themeId = activeThemes[0];
if (themeId == null && activeThemes.Length > 1 && i + 1 < activeThemes.Length)
{
themeId = JsonConfig.settings.activeThemes[i + 1];
themeId = activeThemes[i + 1];
}
displayEvents[i].currentTheme = ThemeManager.themeSettings.Find(t => t.themeId == themeId);
displayEvents[i].displayIndex = JsonConfig.settings.activeThemes[0] == null ? i : -1;
displayEvents[i].displayIndex = activeThemes[0] == null ? i : -1;
}
else
{
string themeId = JsonConfig.settings.lockScreenDisplayIndex != -1 ?
JsonConfig.settings.activeThemes[JsonConfig.settings.lockScreenDisplayIndex] :
string themeId = JsonConfig.settings.lockScreenDisplayIndex >= 0 ?
activeThemes.ElementAtOrDefault(JsonConfig.settings.lockScreenDisplayIndex) :
JsonConfig.settings.lockScreenTheme;
displayEvents[i].currentTheme = ThemeManager.themeSettings.Find(t => t.themeId == themeId);
displayEvents[i].displayIndex = DisplayEvent.LockScreenIndex;
Expand Down Expand Up @@ -151,14 +174,22 @@ public void HandleResumeFromStandby()

private bool UpdateDisplayList()
{
if (JsonConfig.IsNullOrEmpty(JsonConfig.settings.activeThemes) ||
lock (schedulerLock)
{
return UpdateDisplayListCore();
}
}

private bool UpdateDisplayListCore()
{
if (displayEvents == null || JsonConfig.IsNullOrEmpty(JsonConfig.settings.activeThemes) ||
JsonConfig.settings.activeThemes[0] != null)
{
return false;
}

int numDisplaysBefore = displayEvents.Count - 1;
int numDisplaysAfter = Screen.AllScreens.Length;
int numDisplaysAfter = getDisplayCount();
if (numDisplaysAfter != numDisplaysBefore)
{
LoggingHandler.LogMessage("Number of displays updated from {0} to {1}", numDisplaysBefore,
Expand All @@ -177,7 +208,19 @@ private bool UpdateDisplayList()
displayEvents.RemoveRange(numDisplaysAfter, numDisplaysBefore - numDisplaysAfter);
}

return numDisplaysAfter > numDisplaysBefore;
if (numDisplaysAfter != numDisplaysBefore)
{
// Monitor indices may change after a disconnect, so cached paths can belong to different displays.
foreach (DisplayEvent displayEvent in displayEvents)
{
if (displayEvent != null)
{
displayEvent.lastImagePath = null;
}
}
}

return numDisplaysAfter != numDisplaysBefore;
}

private void HandleDisplayEvent(DisplayEvent e)
Expand All @@ -198,11 +241,11 @@ private void HandleDisplayEvent(DisplayEvent e)
{
if (e.displayIndex != DisplayEvent.LockScreenIndex)
{
UwpDesktop.GetHelper().SetWallpaper(imagePath, e.displayIndex);
getPlatformHelper().SetWallpaper(imagePath, e.displayIndex);
}
else
{
UwpDesktop.GetHelper().SetLockScreen(imagePath);
getPlatformHelper().SetLockScreen(imagePath);
}
e.lastImagePath = imagePath;
}
Expand Down Expand Up @@ -249,45 +292,80 @@ private void HandleTimerEvent(bool updateLocation)

private void OnBackgroundTimerElapsed(object sender, EventArgs e)
{
if (nextUpdateTime.HasValue && DateTime.Now >= nextUpdateTime.Value)
{
LoggingHandler.LogMessage("Scheduler event triggered by timer 2");
HandleTimerEvent(true);
}
HandleTimerElapsed(true);
}

private void OnSchedulerTimerElapsed(object sender, EventArgs e)
{
LoggingHandler.LogMessage("Scheduler event triggered by timer 1");
HandleTimerEvent(true);
HandleTimerElapsed(false);
}

private void HandleTimerElapsed(bool background)
{
// A run already in progress will rearm the timer; do not queue redundant callbacks behind it.
if (!Monitor.TryEnter(schedulerLock))
{
return;
}

try
{
if (!background || (nextUpdateTime.HasValue && DateTime.Now >= nextUpdateTime.Value))
{
LoggingHandler.LogMessage("Scheduler event triggered by timer {0}", background ? 2 : 1);
HandleTimerEvent(true);
}
}
finally
{
Monitor.Exit(schedulerLock);
}
}

private void OnDisplaySettingsChanged(object sender, EventArgs e)
internal void OnDisplaySettingsChanged(object sender, EventArgs e)
{
if (UpdateDisplayList())
lock (schedulerLock)
{
LoggingHandler.LogMessage("Scheduler event triggered by display change");
HandleTimerEvent(false);
if (UpdateDisplayList())
{
LoggingHandler.LogMessage("Scheduler event triggered by display change");
HandleTimerEvent(false);
}
}
}

private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e)
{
if (e.Mode == PowerModes.Resume && (UpdateDisplayList() ||
(nextUpdateTime.HasValue && DateTime.Now >= nextUpdateTime.Value)))
if (e.Mode != PowerModes.Resume)
{
return;
}

lock (schedulerLock)
{
LoggingHandler.LogMessage("Scheduler event triggered by resume from sleep");
HandleTimerEvent(false);
if (UpdateDisplayList() || (nextUpdateTime.HasValue && DateTime.Now >= nextUpdateTime.Value))
{
LoggingHandler.LogMessage("Scheduler event triggered by resume from sleep");
HandleTimerEvent(false);
}
else if (nextUpdateTime.HasValue)
{
// Relative timers exclude sleep time, so rearm even when the deadline is still in the future.
StartTimer(nextUpdateTime.Value);
}
}
}

private void OnSessionSwitch(object sender, SessionSwitchEventArgs e)
{
if (e.Reason == SessionSwitchReason.SessionUnlock && (UpdateDisplayList() ||
(nextUpdateTime.HasValue && DateTime.Now >= nextUpdateTime.Value)))
lock (schedulerLock)
{
LoggingHandler.LogMessage("Scheduler event triggered by user session unlock");
HandleTimerEvent(false);
if (e.Reason == SessionSwitchReason.SessionUnlock && (UpdateDisplayList() ||
(nextUpdateTime.HasValue && DateTime.Now >= nextUpdateTime.Value)))
{
LoggingHandler.LogMessage("Scheduler event triggered by user session unlock");
HandleTimerEvent(false);
}
}
}

Expand Down
17 changes: 9 additions & 8 deletions src/HiddenForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ internal class HiddenForm : Form
private const int WM_POWERBROADCAST = 0x0218;
private const int PBT_POWERSETTINGCHANGE = 0x8013;

[DllImport("user32.dll")]
private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hwndNewParent);

public HiddenForm()
{
this.Load += this.HiddenForm_Load;
Expand Down Expand Up @@ -63,13 +60,18 @@ protected override void WndProc(ref Message m)
case WM_QUERYENDSESSION:
if ((m.LParam & ENDSESSION_CLOSEAPP) != 0)
{
UpdateChecker.RegisterApplicationRestart(null, (int)RestartFlags.RESTART_NO_CRASH |
// Register before Restart Manager commits to closing us. Allow update restarts only.
uint result = UpdateChecker.RegisterApplicationRestart(null, (int)RestartFlags.RESTART_NO_CRASH |
(int)RestartFlags.RESTART_NO_HANG | (int)RestartFlags.RESTART_NO_REBOOT);
if (result != 0)
{
LoggingHandler.LogMessage("Failed to register application restart: HRESULT 0x{0:X8}", result);
}
}
m.Result = new IntPtr(1);
break;
case WM_ENDSESSION:
if ((m.LParam & ENDSESSION_CLOSEAPP) != 0)
if (m.WParam != IntPtr.Zero && (m.LParam & ENDSESSION_CLOSEAPP) != 0)
{
Application.Exit();
}
Expand All @@ -94,9 +96,8 @@ protected override void WndProc(ref Message m)

private void HiddenForm_Load(object source, EventArgs e)
{
const int HWND_MESSAGE = -1;
SetParent(this.Handle, new IntPtr(HWND_MESSAGE));

// Keep a top-level window: message-only windows cannot receive broadcasts or be enumerated
// by Restart Manager. The zero-sized tool window stays out of the taskbar and Alt+Tab.
if (UwpDesktop.IsWinRtSupported())
{
PowerSetting.RegisterWindow(this.Handle);
Expand Down
Loading