From 45bad31fff9ec6dd88398c70c83aff07bd13f6a1 Mon Sep 17 00:00:00 2001 From: lx-s Date: Sat, 7 Mar 2026 16:19:03 +0100 Subject: [PATCH] Added quiet hours execution --- Translations/lang-de.json | 2 + Translations/lang-en.json | 2 + WinMute/QuietHoursTimer.cpp | 316 ++++++++++++++--------------- WinMute/QuietHoursTimer.h | 14 +- WinMute/Settings_QuietHoursDlg.cpp | 70 ++++++- WinMute/VistaAudio.cpp | 2 +- 6 files changed, 227 insertions(+), 179 deletions(-) diff --git a/Translations/lang-de.json b/Translations/lang-de.json index 100fd5d..0514627 100644 --- a/Translations/lang-de.json +++ b/Translations/lang-de.json @@ -103,6 +103,8 @@ "settings.quiet-hours.error.error-while-saving.text": "Während dem Speichern der Ruhezeiteinstellungen trat ein Fehler auf", "settings.quiet-hours.add-edit.add-title": "Ruhezeit hinzufügen", "settings.quiet-hours.add-edit.edit-title": "Ruhezeit bearbeiten", + "settings.quiet-hours.error.overlapping-time-range.title": "Überlappender Zeitraum", + "settings.quiet-hours.error.overlapping-time-range.text": "Dieser Zeitraum überschneidet sich mit einem vorhandenen Eintrag.", "settings.wifi.intro": "Das WLAN basiert Stummschalten ermöglicht es die PC-Lautstärke abhängig von dem Namen des verbundenen WLAN Netzwerks zu steuern.", "settings.wifi.enable": "Aktiviere WLAN-basiertes Stummschalten", "settings.wifi.mute-when-not-in-list": "Stummschalten, wenn das WLAN nicht in der Liste ist", diff --git a/Translations/lang-en.json b/Translations/lang-en.json index 9ecb8e3..f62e52d 100644 --- a/Translations/lang-en.json +++ b/Translations/lang-en.json @@ -113,6 +113,8 @@ "settings.quiet-hours.error.error-while-saving.text": "Something went wrong while saving the settings", "settings.quiet-hours.add-edit.add-title": "Add quiet hours", "settings.quiet-hours.add-edit.edit-title": "Edit quiet hours", + "settings.quiet-hours.error.overlapping-time-range.title": "Overlapping time range", + "settings.quiet-hours.error.overlapping-time-range.text": "This time range overlaps with an existing entry.", "settings.wifi.intro": "\"WLAN Mute\" mutes the computer based on the name of the connected wireless network.", "settings.wifi.enable": "Enable WLAN based muting", "settings.wifi.mute-when-in-list": "Mute if connected WLAN is in list", diff --git a/WinMute/QuietHoursTimer.cpp b/WinMute/QuietHoursTimer.cpp index 2c01972..56b997c 100644 --- a/WinMute/QuietHoursTimer.cpp +++ b/WinMute/QuietHoursTimer.cpp @@ -35,80 +35,48 @@ POSSIBILITY OF SUCH DAMAGE. extern HINSTANCE hglobInstance; -static constexpr int QUIETHOURS_TIMER_START_ID = 271020; -static constexpr int QUIETHOURS_TIMER_END_ID = 271021; +static constexpr UINT_PTR QUIETHOURS_TIMER_START_ID = 271020; +static constexpr UINT_PTR QUIETHOURS_TIMER_END_ID = 271021; +static constexpr DWORD SECONDS_PER_DAY = 86400; -static std::int64_t ConvertSystemTimeTo100NS(const LPSYSTEMTIME sysTime) noexcept +// Returns seconds since midnight for the current local time +static DWORD GetNowSeconds() noexcept { - FILETIME ft; - SystemTimeToFileTime(sysTime, &ft); - ULARGE_INTEGER ulInt; - ulInt.HighPart = ft.dwHighDateTime; - ulInt.LowPart = ft.dwLowDateTime; - return static_cast(ulInt.QuadPart); -} - -static bool IsQuietHours( - const LPSYSTEMTIME now, - const LPSYSTEMTIME qhStart, - const LPSYSTEMTIME qhEnd) -{ - const std::int64_t iStart = ConvertSystemTimeTo100NS(qhStart); - std::int64_t iEnd = ConvertSystemTimeTo100NS(qhEnd); - const std::int64_t iNow = ConvertSystemTimeTo100NS(now); - - if (iEnd < iStart) { - // Add one day - iEnd += 864000000000ll; - } - - if (iEnd - iNow > 0 && iStart - iNow < 0) { - return true; - } - - return false; + SYSTEMTIME now; + GetLocalTime(&now); + return static_cast(now.wHour) * 3600 + + static_cast(now.wMinute) * 60 + + static_cast(now.wSecond); } -/** - * Gets the number of milliseconds that t2 would have to add, to reach t1. - * Takes day wrap around into consideration. - */ -static int GetDiffMillseconds(const LPSYSTEMTIME t1, const LPSYSTEMTIME t2) +// Returns true if t falls within [wStart, wEnd) on the circular 24-hour clock +static bool TimeInWindow(DWORD t, DWORD wStart, DWORD wEnd) noexcept { - __int64 it1 = ConvertSystemTimeTo100NS(t1); - __int64 it2 = ConvertSystemTimeTo100NS(t2); - - // 100NS to Milliseconds; - it1 /= 10000; - it2 /= 10000; - if (it1 < it2) { // add one day wrap around - it1 += 24 * 60 * 60 * 1000; + if (wStart < wEnd) { + return t >= wStart && t < wEnd; + } else { // wraps midnight + return t >= wStart || t < wEnd; } - __int64 res = it1 - it2; - - return static_cast(res); } -static VOID CALLBACK QuietHoursTimerProc(HWND hWnd, UINT msg, UINT_PTR id, DWORD msSinceSysStart) noexcept +static VOID CALLBACK QuietHoursTimerProc( + HWND hWnd, UINT /*msg*/, UINT_PTR id, DWORD /*msSinceSysStart*/) noexcept { - UNREFERENCED_PARAMETER(msg); - UNREFERENCED_PARAMETER(msSinceSysStart); + KillTimer(hWnd, id); if (id == QUIETHOURS_TIMER_START_ID) { - KillTimer(hWnd, id); SendMessageW(hWnd, WM_WINMUTE_QUIETHOURS_START, 0, 0); - } - else if (id == QUIETHOURS_TIMER_END_ID) { - KillTimer(hWnd, id); + } else if (id == QUIETHOURS_TIMER_END_ID) { SendMessageW(hWnd, WM_WINMUTE_QUIETHOURS_END, 0, 0); } } +// --------------------------------------------------------------------------- + QuietHoursTimer::QuietHoursTimer() : - initialized_{ false }, - enabled_{ false }, - hParent_{ nullptr }, - qhStart_{0}, - qhEnd_{ 0 } + hParent_(nullptr), + initialized_(false), + enabled_(false), + activeWindowIdx_(-1) { } @@ -120,149 +88,163 @@ QuietHoursTimer::~QuietHoursTimer() } } -bool QuietHoursTimer::LoadFromSettings(const WMSettings& settings) +// Returns the index of the window that currently contains the local time, +// or -1 if none is active. +int QuietHoursTimer::FindActiveWindowIdx() const { - KillTimer(hParent_, QUIETHOURS_TIMER_START_ID); - KillTimer(hParent_, QUIETHOURS_TIMER_END_ID); - - if (!settings.QueryValue(SettingsKey::QUIETHOURS_ENABLE)) { - enabled_ = false; - return true; + const DWORD now = GetNowSeconds(); + for (int i = 0; i < static_cast(windows_.size()); ++i) { + if (TimeInWindow(now, windows_[i].first, windows_[i].second)) { + return i; + } } + return -1; +} - qhStart_ = settings.QueryValue(SettingsKey::QUIETHOURS_START_V0_DEPRECATED); - qhEnd_ = settings.QueryValue(SettingsKey::QUIETHOURS_END_V0_DEPRECATED); - - SYSTEMTIME now; - SYSTEMTIME start; - SYSTEMTIME end; - GetLocalTime(&now); - GetLocalTime(&start); - GetLocalTime(&end); - - start.wSecond = static_cast(qhStart_ % 60); - start.wMinute = static_cast(((qhStart_ - start.wSecond) / 60) % 60); - start.wHour = static_cast((qhStart_ - start.wMinute - start.wSecond) / 3600); - - end.wSecond = static_cast(qhEnd_ % 60); - end.wMinute = static_cast(((qhEnd_ - end.wSecond) / 60) % 60); - end.wHour = static_cast((qhEnd_ - end.wMinute - end.wSecond) / 3600); - - if (IsQuietHours(&now, &start, &end)) { - WMLog::GetInstance().LogInfo(L"Mute: On | Quiet hours have already started"); - SendMessage(hParent_, WM_WINMUTE_QUIETHOURS_START, 0, 0); - } else { - int timerQhStart = GetDiffMillseconds(&start, &now); - if (SetTimer( - hParent_, - QUIETHOURS_TIMER_START_ID, - timerQhStart, - QuietHoursTimerProc) == 0) { - TaskDialog( - hParent_, - hglobInstance, - PROGRAM_NAME, - WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-start.title").c_str(), - WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-start.text").c_str(), - TDCBF_OK_BUTTON, - TD_ERROR_ICON, - nullptr); - return false; +// Returns the index of the window whose start time is soonest in the future +// (wrapping around midnight if necessary). +int QuietHoursTimer::FindNextWindowIdx() const +{ + if (windows_.empty()) return -1; + + const DWORD now = GetNowSeconds(); + int bestIdx = -1; + DWORD bestDiff = MAXDWORD; + + for (int i = 0; i < static_cast(windows_.size()); ++i) { + const DWORD s = windows_[i].first; + const DWORD diff = (s >= now) ? (s - now) : (SECONDS_PER_DAY - now + s); + if (diff < bestDiff) { + bestDiff = diff; + bestIdx = i; } } - return true; + return bestIdx; } bool QuietHoursTimer::SetStart() { - SYSTEMTIME now; - SYSTEMTIME start; + KillTimer(hParent_, QUIETHOURS_TIMER_START_ID); - GetLocalTime(&now); - GetLocalTime(&start); - - start.wSecond = static_cast(qhStart_ % 60); - start.wMinute = static_cast(((qhStart_ - start.wSecond) / 60) % 60); - start.wHour = static_cast((qhStart_ - start.wMinute - start.wSecond) / 3600); - - const int timerQhStart = GetDiffMillseconds(&start, &now); - if (SetTimer( - hParent_, - QUIETHOURS_TIMER_START_ID, - timerQhStart, - QuietHoursTimerProc) == 0) { + if (!enabled_ || windows_.empty()) return true; + + const int idx = FindNextWindowIdx(); + if (idx == -1) return true; + + activeWindowIdx_ = idx; + + const DWORD now = GetNowSeconds(); + const DWORD startSec = windows_[idx].first; + const DWORD diffSec = (startSec >= now) + ? (startSec - now) + : (SECONDS_PER_DAY - now + startSec); + + // diffSec of 0 shouldn't happen here (caller checked FindActiveWindowIdx first), + // but guard against it so the timer fires without an infinite-delay edge case. + const UINT msDelay = (diffSec > 0) ? (diffSec * 1000u) : 1u; + + if (SetTimer(hParent_, QUIETHOURS_TIMER_START_ID, msDelay, QuietHoursTimerProc) == 0) { TaskDialog( - hParent_, - hglobInstance, - PROGRAM_NAME, + hParent_, hglobInstance, PROGRAM_NAME, WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-start.title").c_str(), WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-start.text").c_str(), - TDCBF_OK_BUTTON, - TD_ERROR_ICON, - nullptr); + TDCBF_OK_BUTTON, TD_ERROR_ICON, nullptr); return false; } + + WMLog::GetInstance().LogInfo( + L"QuietHours: Next start scheduled in %u s (window %d, %02u:%02u-%02u:%02u)", + diffSec, idx, + windows_[idx].first / 3600, (windows_[idx].first % 3600) / 60, + windows_[idx].second / 3600, (windows_[idx].second % 3600) / 60); + return true; } bool QuietHoursTimer::SetEnd() { - SYSTEMTIME now; - SYSTEMTIME end; + KillTimer(hParent_, QUIETHOURS_TIMER_END_ID); - GetLocalTime(&now); - GetLocalTime(&end); - - end.wSecond = static_cast(qhEnd_ % 60); - end.wMinute = static_cast(((qhEnd_ - end.wSecond) / 60) % 60); - end.wHour = static_cast((qhEnd_ - end.wMinute - end.wSecond) / 3600); - - int timerQhEnd = GetDiffMillseconds(&end, &now); - if (timerQhEnd <= 0) { - SendMessage(hParent_, WM_WINMUTE_QUIETHOURS_END, 0, 0); - } else if (SetTimer( - hParent_, - QUIETHOURS_TIMER_END_ID, - timerQhEnd, - QuietHoursTimerProc) == 0) { + if (!enabled_ || windows_.empty() || activeWindowIdx_ < 0) return true; + + const DWORD now = GetNowSeconds(); + const DWORD endSec = windows_[activeWindowIdx_].second; + const DWORD diffSec = (endSec > now) + ? (endSec - now) + : (SECONDS_PER_DAY - now + endSec); + + if (diffSec == 0) { + // End is right now — fire the message immediately instead of a 0-ms timer + SendMessageW(hParent_, WM_WINMUTE_QUIETHOURS_END, 0, 0); + return true; + } + + if (SetTimer(hParent_, QUIETHOURS_TIMER_END_ID, diffSec * 1000u, QuietHoursTimerProc) == 0) { TaskDialog( - hParent_, - hglobInstance, - PROGRAM_NAME, - WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-start.title").c_str(), - WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-start.text").c_str(), - TDCBF_OK_BUTTON, - TD_ERROR_ICON, - nullptr); + hParent_, hglobInstance, PROGRAM_NAME, + WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-stop.title").c_str(), + WMi18n::GetInstance().GetTranslationW("popup.error.quiet-hours-stop.text").c_str(), + TDCBF_OK_BUTTON, TD_ERROR_ICON, nullptr); return false; } + + WMLog::GetInstance().LogInfo( + L"QuietHours: End scheduled in %u s (window %d)", diffSec, activeWindowIdx_); + return true; } -bool QuietHoursTimer::Init(HWND hParent, const WMSettings& settings) +bool QuietHoursTimer::IsQuietTime() const { - if (initialized_) { - return true; - } - hParent_ = hParent; - initialized_ = true; - return LoadFromSettings(settings); + if (!enabled_ || windows_.empty()) return false; + return FindActiveWindowIdx() != -1; } -bool QuietHoursTimer::IsQuietTime() +bool QuietHoursTimer::LoadFromSettings(WMSettings& settings) { - SYSTEMTIME now; - SYSTEMTIME start; - SYSTEMTIME end; - GetLocalTime(&now); - GetLocalTime(&start); - GetLocalTime(&end); - return IsQuietHours(&now, &start, &end); + KillTimer(hParent_, QUIETHOURS_TIMER_START_ID); + KillTimer(hParent_, QUIETHOURS_TIMER_END_ID); + windows_.clear(); + activeWindowIdx_ = -1; + + enabled_ = settings.QueryValue(SettingsKey::QUIETHOURS_ENABLE) != 0; + if (!enabled_) return true; + + // Load all defined time windows and sort them by start time + auto loaded = settings.GetQuietHoursTimes(); + for (const auto& t : loaded) { + if (t.first != t.second) { // skip degenerate entries + windows_.push_back(t); + } + } + if (windows_.empty()) return true; + + std::sort(windows_.begin(), windows_.end(), + [](const auto& a, const auto& b) { return a.first < b.first; }); + + // Check if we are already inside a quiet-hours window at startup + const int activeIdx = FindActiveWindowIdx(); + if (activeIdx != -1) { + activeWindowIdx_ = activeIdx; + WMLog::GetInstance().LogInfo(L"QuietHours: Currently inside window %d — muting now", activeIdx); + // Sending START will cause WinMute to mute and call SetEnd() + SendMessageW(hParent_, WM_WINMUTE_QUIETHOURS_START, 0, 0); + } else { + SetStart(); + } + + return true; } -bool QuietHoursTimer::Reset(const WMSettings& settings) +bool QuietHoursTimer::Init(HWND hParent, WMSettings& settings) { + if (initialized_) return true; + hParent_ = hParent; + initialized_ = true; return LoadFromSettings(settings); } - +bool QuietHoursTimer::Reset(WMSettings& settings) +{ + return LoadFromSettings(settings); +} diff --git a/WinMute/QuietHoursTimer.h b/WinMute/QuietHoursTimer.h index 8622208..928b307 100644 --- a/WinMute/QuietHoursTimer.h +++ b/WinMute/QuietHoursTimer.h @@ -45,21 +45,23 @@ public: QuietHoursTimer(const QuietHoursTimer&) = delete; QuietHoursTimer& operator=(const QuietHoursTimer&) = delete; - bool Init(HWND hParent, const WMSettings& settings); + bool Init(HWND hParent, WMSettings& settings); - bool IsQuietTime(); + bool IsQuietTime() const; bool SetStart(); bool SetEnd(); - bool Reset(const WMSettings& settings); + bool Reset(WMSettings& settings); private: HWND hParent_; bool initialized_; bool enabled_; - time_t qhStart_; - time_t qhEnd_; + std::vector> windows_; // sorted by start, seconds since midnight + int activeWindowIdx_; // index of currently scheduled/active window - bool LoadFromSettings(const WMSettings& settings); + bool LoadFromSettings(WMSettings& settings); + int FindActiveWindowIdx() const; + int FindNextWindowIdx() const; }; diff --git a/WinMute/Settings_QuietHoursDlg.cpp b/WinMute/Settings_QuietHoursDlg.cpp index dbaa51d..9dd1725 100644 --- a/WinMute/Settings_QuietHoursDlg.cpp +++ b/WinMute/Settings_QuietHoursDlg.cpp @@ -96,6 +96,57 @@ static void FreeListViewEntries(HWND hListView) } } +static bool TimeInWindow(DWORD t, DWORD wStart, DWORD wEnd) noexcept +{ + if (wStart < wEnd) { + return t >= wStart && t < wEnd; + } else { // wraps midnight + return t >= wStart || t < wEnd; + } +} + +static bool WindowsOverlap(DWORD s1, DWORD e1, DWORD s2, DWORD e2) noexcept +{ + // Two circular intervals overlap iff one contains the start of the other + return TimeInWindow(s1, s2, e2) || TimeInWindow(s2, s1, e1); +} + +static bool ShowOverlapError(HWND hParent) +{ + WMi18n &i18n = WMi18n::GetInstance(); + TaskDialog( + hParent, + nullptr, + PROGRAM_NAME, + i18n.GetTranslationW("settings.quiet-hours.error.overlapping-time-range.title").c_str(), + i18n.GetTranslationW("settings.quiet-hours.error.overlapping-time-range.text").c_str(), + TDCBF_OK_BUTTON, + TD_WARNING_ICON, + nullptr); + return false; +} + +static bool HasOverlapWithListView( + HWND hListView, + const QuietHoursEntry* newEntry, + int excludeIdx) +{ + const int count = ListView_GetItemCount(hListView); + for (int i = 0; i < count; ++i) { + if (i == excludeIdx) continue; + LVITEM lvi{}; + lvi.mask = LVIF_PARAM; + lvi.iItem = i; + if (ListView_GetItem(hListView, &lvi) && lvi.lParam != 0) { + const auto* existing = reinterpret_cast(lvi.lParam); + if (WindowsOverlap(newEntry->start, newEntry->end, existing->start, existing->end)) { + return true; + } + } + } + return false; +} + static bool SaveQuietHours( WMSettings* settings, const int enabled, @@ -383,8 +434,13 @@ INT_PTR CALLBACK Settings_QuietHoursDlgProc(HWND hDlg, UINT msg, WPARAM wParam, hDlg, Settings_QuietHoursAddDlgProc, reinterpret_cast(qh_entry)) == 0) { - AddQuietHoursEntryToListView(hQuietHoursTimes, qh_entry); - UpdateButtonStates(hDlg, hQuietHoursTimes, true); + if (HasOverlapWithListView(hQuietHoursTimes, qh_entry, -1)) { + ShowOverlapError(hDlg); + delete qh_entry; + } else { + AddQuietHoursEntryToListView(hQuietHoursTimes, qh_entry); + UpdateButtonStates(hDlg, hQuietHoursTimes, true); + } } else { delete qh_entry; } @@ -404,9 +460,13 @@ INT_PTR CALLBACK Settings_QuietHoursDlgProc(HWND hDlg, UINT msg, WPARAM wParam, hDlg, Settings_QuietHoursAddDlgProc, reinterpret_cast(&editCopy)) == 0) { - entry->start = editCopy.start; - entry->end = editCopy.end; - UpdateListViewItem(hQuietHoursTimes, sel, entry); + if (HasOverlapWithListView(hQuietHoursTimes, &editCopy, sel)) { + ShowOverlapError(hDlg); + } else { + entry->start = editCopy.start; + entry->end = editCopy.end; + UpdateListViewItem(hQuietHoursTimes, sel, entry); + } } } } diff --git a/WinMute/VistaAudio.cpp b/WinMute/VistaAudio.cpp index 17c621a..40e649f 100644 --- a/WinMute/VistaAudio.cpp +++ b/WinMute/VistaAudio.cpp @@ -173,7 +173,7 @@ void VistaAudio::Uninit() deviceEnumerator_->UnregisterEndpointNotificationCallback(mmnAudioEvents_); } deviceEnumerator_.Release(); - SafeRelease(&mmnAudioEvents_); + mmnAudioEvents_.Release(); endpoints_.clear(); } -- 2.43.0