/ src / modules / alwaysontop / AlwaysOnTop / AlwaysOnTop.cpp
AlwaysOnTop.cpp
  1  #include "pch.h"
  2  #include "AlwaysOnTop.h"
  3  
  4  #include <common/display/dpi_aware.h>
  5  #include <common/utils/game_mode.h>
  6  #include <common/utils/excluded_apps.h>
  7  #include <common/utils/resources.h>
  8  #include <common/utils/winapi_error.h>
  9  #include <common/utils/process_path.h>
 10  
 11  #include <common/utils/elevation.h>
 12  #include <Generated Files/resource.h>
 13  
 14  #include <interop/shared_constants.h>
 15  
 16  #include <trace.h>
 17  #include <WinHookEventIDs.h>
 18  
 19  
 20  namespace NonLocalizable
 21  {
 22      const static wchar_t* TOOL_WINDOW_CLASS_NAME = L"AlwaysOnTopWindow";
 23      const static wchar_t* WINDOW_IS_PINNED_PROP = L"AlwaysOnTop_Pinned";
 24  }
 25  
 26  bool isExcluded(HWND window)
 27  {
 28      auto processPath = get_process_path(window);
 29      CharUpperBuffW(processPath.data(), static_cast<DWORD>(processPath.length()));
 30  
 31      return check_excluded_app(window, processPath, AlwaysOnTopSettings::settings().excludedApps);
 32  }
 33  
 34  AlwaysOnTop::AlwaysOnTop(bool useLLKH, DWORD mainThreadId) :
 35      SettingsObserver({SettingId::FrameEnabled, SettingId::Hotkey, SettingId::ExcludeApps}),
 36      m_hinstance(reinterpret_cast<HINSTANCE>(&__ImageBase)),
 37      m_useCentralizedLLKH(useLLKH),
 38      m_mainThreadId(mainThreadId),
 39      m_notificationUtil(std::make_unique<notifications::NotificationUtil>())
 40  {
 41      s_instance = this;
 42      DPIAware::EnableDPIAwarenessForThisProcess();
 43  
 44      if (InitMainWindow())
 45      {
 46          InitializeWinhookEventIds();
 47  
 48          AlwaysOnTopSettings::instance().InitFileWatcher();
 49          AlwaysOnTopSettings::instance().LoadSettings();
 50  
 51          RegisterHotkey();
 52          RegisterLLKH();
 53          
 54          SubscribeToEvents();
 55          StartTrackingTopmostWindows();
 56      }
 57      else
 58      {
 59          Logger::error("Failed to init AlwaysOnTop module");
 60          // TODO: show localized message
 61      }
 62  }
 63  
 64  AlwaysOnTop::~AlwaysOnTop()
 65  {
 66      m_running = false;
 67      m_notificationUtil.reset();
 68  
 69      if (m_hPinEvent)
 70      {
 71          // Needed to unblock MsgWaitForMultipleObjects one last time
 72          SetEvent(m_hPinEvent);
 73          CloseHandle(m_hPinEvent);
 74      }
 75      m_thread.join();
 76  
 77      CleanUp();
 78  }
 79  
 80  bool AlwaysOnTop::InitMainWindow()
 81  {
 82      WNDCLASSEXW wcex{};
 83      wcex.cbSize = sizeof(WNDCLASSEX);
 84      wcex.lpfnWndProc = WndProc_Helper;
 85      wcex.hInstance = m_hinstance;
 86      wcex.lpszClassName = NonLocalizable::TOOL_WINDOW_CLASS_NAME;
 87      RegisterClassExW(&wcex);
 88  
 89      m_window = CreateWindowExW(WS_EX_TOOLWINDOW, NonLocalizable::TOOL_WINDOW_CLASS_NAME, L"", WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, m_hinstance, this);
 90      if (!m_window)
 91      {
 92          Logger::error(L"Failed to create AlwaysOnTop window: {}", get_last_error_or_default(GetLastError()));
 93          return false;
 94      }
 95  
 96      return true;
 97  }
 98  
 99  void AlwaysOnTop::SettingsUpdate(SettingId id)
100  {
101      switch (id)
102      {
103      case SettingId::Hotkey:
104      {
105          RegisterHotkey();
106      }
107      break;
108      case SettingId::FrameEnabled:
109      {
110          if (AlwaysOnTopSettings::settings().enableFrame)
111          {
112              for (auto& iter : m_topmostWindows)
113              {
114                  if (!iter.second)
115                  {
116                      AssignBorder(iter.first);
117                  }
118              }
119          }
120          else
121          {
122              for (auto& iter : m_topmostWindows)
123              {
124                  iter.second = nullptr;
125              }
126          }    
127      }
128      break;
129      case SettingId::ExcludeApps:
130      {
131          std::vector<HWND> toErase{};
132          for (const auto& [window, border] : m_topmostWindows)
133          {
134              if (isExcluded(window))
135              {
136                  UnpinTopmostWindow(window);
137                  toErase.push_back(window);
138              }
139          }
140  
141          for (const auto window: toErase)
142          {
143              m_topmostWindows.erase(window);
144          }
145      }
146      break;
147      default:
148          break;
149      }
150  }
151  
152  LRESULT AlwaysOnTop::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept
153  {
154      if (message == WM_HOTKEY)
155      {
156          int hotkeyId = static_cast<int>(wparam);
157          if (HWND fw{ GetForegroundWindow() })
158          {
159              if (hotkeyId == static_cast<int>(HotkeyId::Pin))
160              {
161                  ProcessCommand(fw);
162              }
163              else if (hotkeyId == static_cast<int>(HotkeyId::IncreaseOpacity))
164              {
165                  StepWindowTransparency(fw, Settings::transparencyStep);
166              }
167              else if (hotkeyId == static_cast<int>(HotkeyId::DecreaseOpacity))
168              {
169                  StepWindowTransparency(fw, -Settings::transparencyStep);
170              }
171          }
172      }
173      else if (message == WM_PRIV_SETTINGS_CHANGED)
174      {
175          AlwaysOnTopSettings::instance().LoadSettings();
176      }
177      
178      return 0;
179  }
180  
181  void AlwaysOnTop::ProcessCommand(HWND window)
182  {
183      bool gameMode = detect_game_mode();
184      if (AlwaysOnTopSettings::settings().blockInGameMode && gameMode)
185      {
186          return;
187      }
188  
189      if (isExcluded(window))
190      {
191          return;
192      }
193  
194      Sound::Type soundType = Sound::Type::Off;
195      bool topmost = IsTopmost(window);
196      if (topmost)
197      {
198          if (UnpinTopmostWindow(window))
199          {
200              auto iter = m_topmostWindows.find(window);
201              if (iter != m_topmostWindows.end())
202              {
203                  m_topmostWindows.erase(iter);
204              }
205  
206              // Restore transparency when unpinning
207              RestoreWindowAlpha(window);
208              m_windowOriginalLayeredState.erase(window);
209  
210              Trace::AlwaysOnTop::UnpinWindow();
211          }
212      }
213      else
214      {
215          if (PinTopmostWindow(window))
216          {
217              soundType = Sound::Type::On;
218              AssignBorder(window);
219              
220              Trace::AlwaysOnTop::PinWindow();
221          }
222      }
223  
224      if (AlwaysOnTopSettings::settings().enableSound)
225      {
226          m_sound.Play(soundType);    
227      }
228  }
229  
230  void AlwaysOnTop::StartTrackingTopmostWindows()
231  {
232      using result_t = std::vector<HWND>;
233      result_t result;
234  
235      auto enumWindows = [](HWND hwnd, LPARAM param) -> BOOL {
236          if (!IsWindowVisible(hwnd))
237          {
238              return TRUE;
239          }
240  
241          if (isExcluded(hwnd))
242          {
243              return TRUE;
244          }
245  
246          auto windowName = GetWindowTextLength(hwnd);
247          if (windowName > 0)
248          {
249              result_t& result = *reinterpret_cast<result_t*>(param);
250              result.push_back(hwnd);
251          }
252  
253          return TRUE;
254      };
255  
256      EnumWindows(enumWindows, reinterpret_cast<LPARAM>(&result));
257  
258      for (HWND window : result)
259      {
260          if (IsPinned(window))
261          {
262              AssignBorder(window);
263          }
264      }
265  }
266  
267  bool AlwaysOnTop::AssignBorder(HWND window)
268  {
269      if (m_virtualDesktopUtils.IsWindowOnCurrentDesktop(window) && AlwaysOnTopSettings::settings().enableFrame)
270      {
271          auto border = WindowBorder::Create(window, m_hinstance);
272          if (border)
273          {
274              m_topmostWindows[window] = std::move(border);
275          }
276      }
277      else
278      {
279          m_topmostWindows[window] = nullptr;
280      }
281      
282      return true;
283  }
284  
285  void AlwaysOnTop::RegisterHotkey() const
286  {
287      if (m_useCentralizedLLKH)
288      {
289          // All hotkeys are handled by centralized LLKH
290          return;
291      }
292  
293      // Register hotkeys only when not using centralized LLKH
294      UnregisterHotKey(m_window, static_cast<int>(HotkeyId::Pin));
295      UnregisterHotKey(m_window, static_cast<int>(HotkeyId::IncreaseOpacity));
296      UnregisterHotKey(m_window, static_cast<int>(HotkeyId::DecreaseOpacity));
297  
298      // Register pin hotkey
299      RegisterHotKey(m_window, static_cast<int>(HotkeyId::Pin), AlwaysOnTopSettings::settings().hotkey.get_modifiers(), AlwaysOnTopSettings::settings().hotkey.get_code());
300  
301      // Register transparency hotkeys using the same modifiers as the pin hotkey
302      UINT modifiers = AlwaysOnTopSettings::settings().hotkey.get_modifiers();
303      RegisterHotKey(m_window, static_cast<int>(HotkeyId::IncreaseOpacity), modifiers, VK_OEM_PLUS);
304      RegisterHotKey(m_window, static_cast<int>(HotkeyId::DecreaseOpacity), modifiers, VK_OEM_MINUS);
305  }
306  
307  void AlwaysOnTop::RegisterLLKH()
308  {
309      if (!m_useCentralizedLLKH)
310      {
311          return;
312      }
313  	
314      m_hPinEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT);
315      m_hTerminateEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_TERMINATE_EVENT);
316      m_hIncreaseOpacityEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_INCREASE_OPACITY_EVENT);
317      m_hDecreaseOpacityEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_DECREASE_OPACITY_EVENT);
318  
319      if (!m_hPinEvent)
320      {
321          Logger::warn(L"Failed to create pinEvent. {}", get_last_error_or_default(GetLastError()));
322          return;
323      }
324  
325      if (!m_hTerminateEvent)
326      {
327          Logger::warn(L"Failed to create terminateEvent. {}", get_last_error_or_default(GetLastError()));
328          return;
329      }
330  
331      if (!m_hIncreaseOpacityEvent)
332      {
333          Logger::warn(L"Failed to create increaseOpacityEvent. {}", get_last_error_or_default(GetLastError()));
334      }
335  
336      if (!m_hDecreaseOpacityEvent)
337      {
338          Logger::warn(L"Failed to create decreaseOpacityEvent. {}", get_last_error_or_default(GetLastError()));
339      }
340  
341      HANDLE handles[4] = { m_hPinEvent,
342                            m_hTerminateEvent,
343                            m_hIncreaseOpacityEvent,
344                            m_hDecreaseOpacityEvent };
345  
346      m_thread = std::thread([this, handles]() {
347          MSG msg;
348          while (m_running)
349          {
350              DWORD dwEvt = MsgWaitForMultipleObjects(4, handles, false, INFINITE, QS_ALLINPUT);
351              if (!m_running)
352              {
353                  break;
354              }
355              switch (dwEvt)
356              {
357              case WAIT_OBJECT_0: // Pin event
358                  if (HWND fw{ GetForegroundWindow() })
359                  {
360                      ProcessCommand(fw);
361                  }
362                  break;
363              case WAIT_OBJECT_0 + 1: // Terminate event
364                  PostThreadMessage(m_mainThreadId, WM_QUIT, 0, 0);
365                  break;
366              case WAIT_OBJECT_0 + 2: // Increase opacity event
367                  if (HWND fw{ GetForegroundWindow() })
368                  {
369                      StepWindowTransparency(fw, Settings::transparencyStep);
370                  }
371                  break;
372              case WAIT_OBJECT_0 + 3: // Decrease opacity event
373                  if (HWND fw{ GetForegroundWindow() })
374                  {
375                      StepWindowTransparency(fw, -Settings::transparencyStep);
376                  }
377                  break;
378              case WAIT_OBJECT_0 + 4: // Message queue
379                  if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
380                  {
381                      TranslateMessage(&msg);
382                      DispatchMessageW(&msg);
383                  }
384                  break;
385              default:
386                  break;
387              }
388          }
389      });
390  }
391  
392  void AlwaysOnTop::SubscribeToEvents()
393  {
394      // subscribe to windows events
395      std::array<DWORD, 7> events_to_subscribe = {
396          EVENT_OBJECT_LOCATIONCHANGE,
397          EVENT_SYSTEM_MINIMIZESTART,
398          EVENT_SYSTEM_MINIMIZEEND,
399          EVENT_SYSTEM_MOVESIZEEND,
400          EVENT_SYSTEM_FOREGROUND,
401          EVENT_OBJECT_DESTROY,
402          EVENT_OBJECT_FOCUS,
403      };
404  
405      for (const auto event : events_to_subscribe)
406      {
407          auto hook = SetWinEventHook(event, event, nullptr, WinHookProc, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
408          if (hook)
409          {
410              m_staticWinEventHooks.emplace_back(hook);
411          }
412          else
413          {
414              Logger::error(L"Failed to set win event hook");
415          }
416      }
417  }
418  
419  void AlwaysOnTop::UnpinAll()
420  {
421      for (const auto& [topWindow, border] : m_topmostWindows)
422      {
423          if (!UnpinTopmostWindow(topWindow))
424          {
425              Logger::error(L"Unpinning topmost window failed");
426          }
427          // Restore transparency when unpinning all
428          RestoreWindowAlpha(topWindow);
429      }
430  
431      m_topmostWindows.clear();
432      m_windowOriginalLayeredState.clear();
433  }
434  
435  void AlwaysOnTop::CleanUp()
436  {
437      UnpinAll();
438      if (m_window)
439      {
440          DestroyWindow(m_window);
441          m_window = nullptr;
442      }
443  
444      UnregisterClass(NonLocalizable::TOOL_WINDOW_CLASS_NAME, reinterpret_cast<HINSTANCE>(&__ImageBase));
445  }
446  
447  bool AlwaysOnTop::IsTopmost(HWND window) const noexcept
448  {
449      int exStyle = GetWindowLong(window, GWL_EXSTYLE);
450      return (exStyle & WS_EX_TOPMOST) == WS_EX_TOPMOST;
451  }
452  
453  bool AlwaysOnTop::IsPinned(HWND window) const noexcept
454  {
455      auto handle = GetProp(window, NonLocalizable::WINDOW_IS_PINNED_PROP);
456      return (handle != NULL);
457  }
458  
459  bool AlwaysOnTop::PinTopmostWindow(HWND window) const noexcept
460  {
461      if (!SetProp(window, NonLocalizable::WINDOW_IS_PINNED_PROP, reinterpret_cast<HANDLE>(1)))
462      {
463          Logger::error(L"SetProp failed, {}", get_last_error_or_default(GetLastError()));
464      }
465  
466      auto res = SetWindowPos(window, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
467      if (!res)
468      {
469          Logger::error(L"Failed to pin window, {}", get_last_error_or_default(GetLastError()));
470      }
471  
472      return res;
473  }
474  
475  bool AlwaysOnTop::UnpinTopmostWindow(HWND window) const noexcept
476  {
477      RemoveProp(window, NonLocalizable::WINDOW_IS_PINNED_PROP);
478      auto res = SetWindowPos(window, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
479      if (!res)
480      {
481          Logger::error(L"Failed to unpin window, {}", get_last_error_or_default(GetLastError()));
482      }
483  
484      return res;
485  }
486  
487  bool AlwaysOnTop::IsTracked(HWND window) const noexcept
488  {
489      auto iter = m_topmostWindows.find(window);
490      return (iter != m_topmostWindows.end());
491  }
492  
493  void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
494  {
495      if (!AlwaysOnTopSettings::settings().enableFrame || !data->hwnd)
496      {
497          return;
498      }
499  
500      std::vector<HWND> toErase{};
501      for (const auto& [window, border] : m_topmostWindows)
502      {
503          // check if the window was closed, since for some EVENT_OBJECT_DESTROY doesn't work
504          // fixes https://github.com/microsoft/PowerToys/issues/15300
505          bool visible = IsWindowVisible(window);
506          if (!visible)
507          {
508              UnpinTopmostWindow(window);
509              toErase.push_back(window);
510          }
511      }
512  
513      for (const auto window : toErase)
514      {
515          m_topmostWindows.erase(window);
516          m_windowOriginalLayeredState.erase(window);
517      }
518  
519      switch (data->event)
520      {
521      case EVENT_OBJECT_LOCATIONCHANGE:
522      {
523          auto iter = m_topmostWindows.find(data->hwnd);
524          if (iter != m_topmostWindows.end())
525          {
526              const auto& border = iter->second;
527              if (border)
528              {
529                  border->UpdateBorderPosition();
530              }
531          }
532      }
533      break;
534      case EVENT_SYSTEM_MINIMIZESTART:
535      {
536          auto iter = m_topmostWindows.find(data->hwnd);
537          if (iter != m_topmostWindows.end())
538          {
539              m_topmostWindows[data->hwnd] = nullptr;
540          }
541      }
542      break;
543      case EVENT_SYSTEM_MINIMIZEEND:
544      {
545          auto iter = m_topmostWindows.find(data->hwnd);
546          if (iter != m_topmostWindows.end())
547          {
548              // pin border again, in some cases topmost flag stops working: https://github.com/microsoft/PowerToys/issues/17332
549              PinTopmostWindow(data->hwnd); 
550              AssignBorder(data->hwnd);
551          }
552      }
553      break;
554      case EVENT_SYSTEM_MOVESIZEEND:
555      {
556          auto iter = m_topmostWindows.find(data->hwnd);
557          if (iter != m_topmostWindows.end())
558          {
559              const auto& border = iter->second;
560              if (border)
561              {
562                  border->UpdateBorderPosition();
563              }
564          }
565      }
566      break;
567      case EVENT_SYSTEM_FOREGROUND:
568      {
569          if (!is_process_elevated() && IsProcessOfWindowElevated(data->hwnd))
570          {
571              m_notificationUtil->WarnIfElevationIsRequired(GET_RESOURCE_STRING(IDS_ALWAYSONTOP),
572                                                            GET_RESOURCE_STRING(IDS_SYSTEM_FOREGROUND_ELEVATED),
573                                                            GET_RESOURCE_STRING(IDS_SYSTEM_FOREGROUND_ELEVATED_LEARN_MORE),
574                                                            GET_RESOURCE_STRING(IDS_SYSTEM_FOREGROUND_ELEVATED_DIALOG_DONT_SHOW_AGAIN));
575          }
576          RefreshBorders();
577      }
578      break;
579      case EVENT_OBJECT_FOCUS:
580      {
581          for (const auto& [window, border] : m_topmostWindows)
582          {
583              // check if topmost was reset
584              // fixes https://github.com/microsoft/PowerToys/issues/19168
585              if (!IsTopmost(window))
586              {
587                  Logger::trace(L"A window no longer has Topmost set and it should. Setting topmost again.");
588                  PinTopmostWindow(window);
589              }
590          }
591      }
592      break;
593      default:
594          break;
595      }
596  }
597  
598  void AlwaysOnTop::RefreshBorders()
599  {
600      for (const auto& [window, border] : m_topmostWindows)
601      {
602          if (m_virtualDesktopUtils.IsWindowOnCurrentDesktop(window))
603          {
604              if (!border)
605              {
606                  AssignBorder(window);
607              }
608          }
609          else
610          {
611              if (border)
612              {
613                  m_topmostWindows[window] = nullptr;
614              }
615          }
616      }
617  }
618  
619  HWND AlwaysOnTop::ResolveTransparencyTargetWindow(HWND window)
620  {
621      if (!window || !IsWindow(window))
622      {
623          return nullptr;
624      }
625  
626      // Only allow transparency changes on pinned windows
627      if (!IsPinned(window))
628      {
629          return nullptr;
630      }
631  
632      return window;
633  }
634  
635  
636  void AlwaysOnTop::StepWindowTransparency(HWND window, int delta)
637  {
638      HWND targetWindow = ResolveTransparencyTargetWindow(window);
639      if (!targetWindow)
640      {
641          return;
642      }
643  
644      int currentTransparency = Settings::maxTransparencyPercentage;
645      LONG exStyle = GetWindowLong(targetWindow, GWL_EXSTYLE);
646      if (exStyle & WS_EX_LAYERED)
647      {
648          BYTE alpha = 255;
649          if (GetLayeredWindowAttributes(targetWindow, nullptr, &alpha, nullptr))
650          {
651              currentTransparency = (alpha * 100) / 255;
652          }
653      }
654  
655      int newTransparency = (std::max)(Settings::minTransparencyPercentage, 
656                                       (std::min)(Settings::maxTransparencyPercentage, currentTransparency + delta));
657  
658      if (newTransparency != currentTransparency)
659      {
660          ApplyWindowAlpha(targetWindow, newTransparency);
661  
662          if (AlwaysOnTopSettings::settings().enableSound)
663          {
664              m_sound.Play(delta > 0 ? Sound::Type::IncreaseOpacity : Sound::Type::DecreaseOpacity);
665          }
666  
667          Logger::debug(L"Transparency adjusted to {}%", newTransparency);
668      }
669  }
670  
671  void AlwaysOnTop::ApplyWindowAlpha(HWND window, int percentage)
672  {
673      if (!window || !IsWindow(window))
674      {
675          return;
676      }
677      
678      percentage = (std::max)(Settings::minTransparencyPercentage, 
679                              (std::min)(Settings::maxTransparencyPercentage, percentage));
680  
681      LONG exStyle = GetWindowLong(window, GWL_EXSTYLE);
682      bool isCurrentlyLayered = (exStyle & WS_EX_LAYERED) != 0;
683  
684      // Cache original state on first transparency application
685      if (m_windowOriginalLayeredState.find(window) == m_windowOriginalLayeredState.end())
686      {
687          WindowLayeredState state;
688          state.hadLayeredStyle = isCurrentlyLayered;
689          
690          if (isCurrentlyLayered)
691          {
692              BYTE alpha = 255;
693              COLORREF colorKey = 0;
694              DWORD flags = 0;
695              if (GetLayeredWindowAttributes(window, &colorKey, &alpha, &flags))
696              {
697                  state.originalAlpha = alpha;
698                  state.usedColorKey = (flags & LWA_COLORKEY) != 0;
699                  state.colorKey = colorKey;
700              }
701              else
702              {
703                  Logger::warn(L"GetLayeredWindowAttributes failed for layered window, skipping");
704                  return;
705              }
706          }
707          m_windowOriginalLayeredState[window] = state;
708      }
709  
710      // Clear WS_EX_LAYERED first to ensure SetLayeredWindowAttributes works
711      if (isCurrentlyLayered)
712      {
713          SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
714          SetWindowPos(window, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
715          exStyle = GetWindowLong(window, GWL_EXSTYLE);
716      }
717  
718      BYTE alphaValue = static_cast<BYTE>((255 * percentage) / 100);
719      SetWindowLong(window, GWL_EXSTYLE, exStyle | WS_EX_LAYERED);
720      SetLayeredWindowAttributes(window, 0, alphaValue, LWA_ALPHA);
721  }
722  
723  void AlwaysOnTop::RestoreWindowAlpha(HWND window)
724  {
725      if (!window || !IsWindow(window))
726      {
727          return;
728      }
729  
730      LONG exStyle = GetWindowLong(window, GWL_EXSTYLE);
731      auto it = m_windowOriginalLayeredState.find(window);
732      
733      if (it != m_windowOriginalLayeredState.end())
734      {
735          const auto& originalState = it->second;
736          
737          if (originalState.hadLayeredStyle)
738          {
739              // Window originally had WS_EX_LAYERED - restore original attributes
740              // Clear and re-add to ensure clean state
741              if (exStyle & WS_EX_LAYERED)
742              {
743                  SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
744                  exStyle = GetWindowLong(window, GWL_EXSTYLE);
745              }
746              SetWindowLong(window, GWL_EXSTYLE, exStyle | WS_EX_LAYERED);
747              
748              // Restore original alpha and/or color key
749              DWORD flags = LWA_ALPHA;
750              if (originalState.usedColorKey)
751              {
752                  flags |= LWA_COLORKEY;
753              }
754              SetLayeredWindowAttributes(window, originalState.colorKey, originalState.originalAlpha, flags);
755              SetWindowPos(window, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
756          }
757          else
758          {
759              // Window originally didn't have WS_EX_LAYERED - remove it completely
760              if (exStyle & WS_EX_LAYERED)
761              {
762                  SetLayeredWindowAttributes(window, 0, 255, LWA_ALPHA);
763                  SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
764                  SetWindowPos(window, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
765              }
766          }
767          
768          m_windowOriginalLayeredState.erase(it);
769      }
770      else
771      {
772          // Fallback: no cached state, just remove layered style
773          if (exStyle & WS_EX_LAYERED)
774          {
775              SetLayeredWindowAttributes(window, 0, 255, LWA_ALPHA);
776              SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
777          }
778      }
779  }