[AlwaysOnTop] Proof of concept (#14360)

Co-authored-by: Niels Laute <niels.laute@live.nl>
This commit is contained in:
Seraphima Zykova
2021-12-29 20:33:20 +03:00
committed by GitHub
parent d39c4121a9
commit fa81968dbb
60 changed files with 2859 additions and 10 deletions

View File

@@ -39,6 +39,7 @@ ALLOWUNDO
ALPHATYPE
Altdown
altform
alwaysontop
amd
Amicrosoft
AModifier
@@ -1169,6 +1170,7 @@ mlcfg
mmdeviceapi
mmi
mmsys
mmsystem
mockapi
MODECHANGE
moderncop
@@ -1325,6 +1327,7 @@ NOTIFYICONDATAW
NOTIMPL
notmatch
Noto
NOTOPMOST
NOTRACK
NOUPDATE
NOZORDER
@@ -1889,6 +1892,8 @@ SVGIO
svgpreviewhandler
SWC
SWFO
Switchbetweenvirtualdesktops
SWITCHEND
SWP
swprintf
SWRESTORE
@@ -1909,6 +1914,7 @@ SYSKEYUP
syslog
SYSMENU
systemd
SYSTEMASTERISK
SYSTEMTIME
Tadele
tadele
@@ -2188,6 +2194,7 @@ Winhook
winkey
WINL
winmd
winmm
WINMSAPP
winnt
winres
@@ -2219,7 +2226,7 @@ WNDPROC
wofstream
wordpad
workaround
Workflow
workflow
workspaces
wostream
wostringstream

View File

@@ -90,6 +90,8 @@ build:
- 'modules\ColorPicker\PowerToys.Interop.dll'
- 'modules\ColorPicker\Telemetry.dll'
- '**\*.resources.dll'
- 'modules\AlwaysOnTop\PowerToys.AlwaysOnTop.exe'
- 'modules\AlwaysOnTop\PowerToys.AlwaysOnTopModuleInterface.dll'
- 'modules\Awake\PowerToys.AwakeModuleInterface.dll'
- 'modules\Awake\PowerToys.ManagedCommon.dll'
- 'modules\Awake\PowerToys.ManagedTelemetry.dll'

View File

@@ -383,6 +383,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GcodePreviewHandler", "src\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests-GcodePreviewHandler", "src\modules\previewpane\UnitTests-GcodePreviewHandler\UnitTests-GcodePreviewHandler.csproj", "{FCF3E52D-B80A-4FC3-98FD-6391354F0EE3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AlwaysOnTop", "AlwaysOnTop", "{60CD2D4F-C3B9-4897-9821-FCA5098B41CE}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AlwaysOnTop", "src\modules\alwaysontop\AlwaysOnTop\AlwaysOnTop.vcxproj", "{1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AlwaysOnTopModuleInterface", "src\modules\alwaysontop\AlwaysOnTopModuleInterface\AlwaysOnTopModuleInterface.vcxproj", "{48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.WebSearch", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.WebSearch\Community.PowerToys.Run.Plugin.WebSearch.csproj", "{9F94B303-5E21-4364-9362-64426F8DB932}"
EndProject
Global
@@ -1030,6 +1036,18 @@ Global
{FCF3E52D-B80A-4FC3-98FD-6391354F0EE3}.Release|x64.ActiveCfg = Release|x64
{FCF3E52D-B80A-4FC3-98FD-6391354F0EE3}.Release|x64.Build.0 = Release|x64
{FCF3E52D-B80A-4FC3-98FD-6391354F0EE3}.Release|x86.ActiveCfg = Release|x64
{1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}.Debug|x64.ActiveCfg = Debug|x64
{1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}.Debug|x64.Build.0 = Debug|x64
{1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}.Debug|x86.ActiveCfg = Debug|x64
{1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}.Release|x64.ActiveCfg = Release|x64
{1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}.Release|x64.Build.0 = Release|x64
{1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}.Release|x86.ActiveCfg = Release|x64
{48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}.Debug|x64.ActiveCfg = Debug|x64
{48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}.Debug|x64.Build.0 = Debug|x64
{48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}.Debug|x86.ActiveCfg = Debug|x64
{48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}.Release|x64.ActiveCfg = Release|x64
{48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}.Release|x64.Build.0 = Release|x64
{48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}.Release|x86.ActiveCfg = Release|x64
{9F94B303-5E21-4364-9362-64426F8DB932}.Debug|x64.ActiveCfg = Debug|x64
{9F94B303-5E21-4364-9362-64426F8DB932}.Debug|x64.Build.0 = Debug|x64
{9F94B303-5E21-4364-9362-64426F8DB932}.Debug|x86.ActiveCfg = Debug|x64
@@ -1160,6 +1178,9 @@ Global
{133281D8-1BCE-4D07-B31E-796612A9609E} = {2F305555-C296-497E-AC20-5FA1B237996A}
{805306FF-A562-4415-8DEF-E493BDC45918} = {2F305555-C296-497E-AC20-5FA1B237996A}
{FCF3E52D-B80A-4FC3-98FD-6391354F0EE3} = {2F305555-C296-497E-AC20-5FA1B237996A}
{60CD2D4F-C3B9-4897-9821-FCA5098B41CE} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{1DC3BE92-CE89-43FB-8110-9C043A2FE7A2} = {60CD2D4F-C3B9-4897-9821-FCA5098B41CE}
{48A0A19E-A0BE-4256-ACF8-CC3B80291AF9} = {60CD2D4F-C3B9-4897-9821-FCA5098B41CE}
{9F94B303-5E21-4364-9362-64426F8DB932} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

@@ -11,6 +11,7 @@
<?define VideoConferenceProjectName="VideoConference"?>
<?define AwakeProjectName="Awake"?>
<?define MouseUtilsProjectName="MouseUtils"?>
<?define AlwaysOnTopProjectName="AlwaysOnTop"?>
<?define RepoDir="$(var.ProjectDir)..\..\" ?>
<?define BinX32Dir="$(var.RepoDir)x86\$(var.Configuration)\" ?>
@@ -304,6 +305,10 @@
<Directory Id="MouseUtilsInstallFolder" Name="$(var.MouseUtilsProjectName)">
</Directory>
<!-- AlwaysOnTop -->
<Directory Id="AlwaysOnTopInstallFolder" Name="$(var.AlwaysOnTopProjectName)">
</Directory>
<!-- Launcher -->
<Directory Id="LauncherInstallFolder" Name="launcher">
<Directory Id="AssetsFolder" Name="Assets" />
@@ -830,6 +835,14 @@
</Component>
</DirectoryRef>
<!-- AlwaysOnTop -->
<DirectoryRef Id="AlwaysOnTopInstallFolder" FileSource="$(var.BinX64Dir)modules\$(var.AlwaysOnTopProjectName)">
<Component Id="Module_AlwaysOnTop" Guid="599D40E7-862A-4A4C-8013-D9CE0BEB3D6C" Win64="yes">
<File Source="$(var.BinX64Dir)modules\$(var.AlwaysOnTopProjectName)\PowerToys.AlwaysOnTopModuleInterface.dll" KeyPath="yes" />
<File Source="$(var.BinX64Dir)modules\$(var.AlwaysOnTopProjectName)\PowerToys.AlwaysOnTop.exe" />
</Component>
</DirectoryRef>
<!-- SettingsV2 components -->
<DirectoryRef Id="SettingsV2InstallFolder" FileSource="$(var.BinX64Dir)Settings\">
<Component Id="SettingsV2" Guid="4B108DC0-4B2C-4AC4-AAA9-1B2DC8399F7C" Win64="yes">
@@ -877,21 +890,21 @@
</DirectoryRef>
<DirectoryRef Id="SettingsV2AssetsModulesInstallFolder" FileSource="$(var.BinX64Dir)Settings\Assets\Modules">
<Component Id="SettingsV2AssetsModules" Guid="A0B961A9-77D0-4223-88A9-E3B41BD9C329" Win64="yes">
<?foreach File in ColorPicker.png;FancyZones.png;Awake.png;ImageResizer.png;KBM.png;MouseUtils.png;PowerLauncher.png;PowerPreview.png;PowerRename.png;PT.png;ShortcutGuide.png;VideoConference.png;Wallpaper.png?>
<?foreach File in ColorPicker.png;FancyZones.png;AlwaysOnTop.png;Awake.png;ImageResizer.png;KBM.png;MouseUtils.png;PowerLauncher.png;PowerPreview.png;PowerRename.png;PT.png;ShortcutGuide.png;VideoConference.png;Wallpaper.png?>
<File Id="SettingsV2AssetsModules_$(var.File)" Source="$(var.BinX64Dir)Settings\Assets\Modules\$(var.File)" />
<?endforeach?>
</Component>
</DirectoryRef>
<DirectoryRef Id="SettingsV2OOBEAssetsModulesInstallFolder" FileSource="$(var.BinX64Dir)Settings\Assets\Modules\OOBE">
<Component Id="SettingsV2OOBEAssetsModules" Guid="E2360A83-6694-4B33-B5F6-641A906359EE" Win64="yes">
<?foreach File in ColorPicker.gif;Awake.png;FancyZones.gif;FileExplorer.png;ImageResizer.gif;KBM.gif;MouseUtils.gif;PowerRename.gif;Run.gif;OOBEShortcutGuide.png;VideoConferenceMute.png;OOBEPTHero.png?>
<?foreach File in ColorPicker.gif;AlwaysOnTop.png;Awake.png;FancyZones.gif;FileExplorer.png;ImageResizer.gif;KBM.gif;MouseUtils.gif;PowerRename.gif;Run.gif;OOBEShortcutGuide.png;VideoConferenceMute.png;OOBEPTHero.png?>
<File Id="SettingsV2OOBEAssetsModules_$(var.File)" Source="$(var.BinX64Dir)Settings\Assets\Modules\OOBE\$(var.File)" />
<?endforeach?>
</Component>
</DirectoryRef>
<DirectoryRef Id="SettingsV2OOBEAssetsFluentIconsInstallFolder" FileSource="$(var.BinX64Dir)Settings\Assets\FluentIcons">
<Component Id="SettingsV2OOBEAssetsFluentIcons" Guid="6A380D5A-DA63-45B5-B68F-06D57CDD1B9C" Win64="yes">
<?foreach File in ColorPicker.png;FancyZones.png;Awake.png;FileExplorerPreview.png;FindMyMouse.png;ImageResizer.png;KeyboardManager.png;MouseHighlighter.png;MouseUtils.png;PowerRename.png;PowerToys.png;PowerToysRun.png;Settings.png;ShortcutGuide.png;VideoConferenceMute.png ?>
<?foreach File in ColorPicker.png;FancyZones.png;AlwaysOnTop.png;Awake.png;FileExplorerPreview.png;FindMyMouse.png;ImageResizer.png;KeyboardManager.png;MouseHighlighter.png;MouseUtils.png;PowerRename.png;PowerToys.png;PowerToysRun.png;Settings.png;ShortcutGuide.png;VideoConferenceMute.png ?>
<File Id="SettingsV2OOBEAssetsFluentIcons_$(var.File)" Source="$(var.BinX64Dir)Settings\Assets\FluentIcons\FluentIcons$(var.File)" />
<?endforeach?>
</Component>
@@ -989,6 +1002,7 @@
<ComponentRef Id="Module_Awake_runtime_netcoreapp21"/>
<ComponentRef Id="Module_FindMyMouse"/>
<ComponentRef Id="Module_MouseHighlighter"/>
<ComponentRef Id="Module_AlwaysOnTop"/>
<ComponentRef Id="SettingsV2" />
<ComponentRef Id="SettingsV2Assets" />
<ComponentRef Id="SettingsV2AssetsModules" />

View File

@@ -938,7 +938,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
}
processes.resize(bytes / sizeof(processes[0]));
std::array<std::wstring_view, 8> processesToTerminate = {
std::array<std::wstring_view, 9> processesToTerminate = {
L"PowerToys.PowerLauncher.exe",
L"PowerToys.Settings.exe",
L"PowerToys.Awake.exe",
@@ -946,6 +946,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
L"PowerToys.Settings.UI.exe",
L"PowerToys.FancyZonesEditor.exe",
L"PowerToys.ColorPickerUI.exe",
L"PowerToys.AlwaysOnTop.exe",
L"PowerToys.exe"
};

View File

@@ -28,6 +28,8 @@ struct LogSettings
inline const static std::string findMyMouseLoggerName = "find-my-mouse";
inline const static std::string mouseHighlighterLoggerName = "mouse-highlighter";
inline const static std::string powerRenameLoggerName = "powerrename";
inline const static std::string alwaysOnTopLoggerName = "always-on-top";
inline const static std::wstring alwaysOnTopLogPath = L"always-on-top-log.txt";
inline const static int retention = 30;
std::wstring logLevel;
LogSettings();

View File

@@ -0,0 +1,415 @@
#include "pch.h"
#include "AlwaysOnTop.h"
#include <common/display/dpi_aware.h>
#include <common/utils/game_mode.h>
#include <common/utils/resources.h>
#include <common/utils/winapi_error.h>
#include <common/utils/process_path.h>
#include <WinHookEventIDs.h>
namespace NonLocalizable
{
const static wchar_t* TOOL_WINDOW_CLASS_NAME = L"AlwaysOnTopWindow";
}
// TODO: move to common utils
bool find_app_name_in_path(const std::wstring& where, const std::vector<std::wstring>& what)
{
for (const auto& row : what)
{
const auto pos = where.rfind(row);
const auto last_slash = where.rfind('\\');
//Check that row occurs in where, and its last occurrence contains in itself the first character after the last backslash.
if (pos != std::wstring::npos && pos <= last_slash + 1 && pos + row.length() > last_slash)
{
return true;
}
}
return false;
}
bool isExcluded(HWND window)
{
auto processPath = get_process_path(window);
CharUpperBuffW(processPath.data(), (DWORD)processPath.length());
return find_app_name_in_path(processPath, AlwaysOnTopSettings::settings().excludedApps);
}
AlwaysOnTop::AlwaysOnTop() :
SettingsObserver({SettingId::FrameEnabled, SettingId::Hotkey, SettingId::ExcludeApps}),
m_hinstance(reinterpret_cast<HINSTANCE>(&__ImageBase))
{
s_instance = this;
DPIAware::EnableDPIAwarenessForThisProcess();
if (InitMainWindow())
{
InitializeWinhookEventIds();
AlwaysOnTopSettings::instance().InitFileWatcher();
AlwaysOnTopSettings::instance().LoadSettings();
RegisterHotkey();
SubscribeToEvents();
StartTrackingTopmostWindows();
}
else
{
Logger::error("Failed to init AlwaysOnTop module");
// TODO: show localized message
}
}
AlwaysOnTop::~AlwaysOnTop()
{
CleanUp();
}
bool AlwaysOnTop::InitMainWindow()
{
WNDCLASSEXW wcex{};
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.lpfnWndProc = WndProc_Helper;
wcex.hInstance = m_hinstance;
wcex.lpszClassName = NonLocalizable::TOOL_WINDOW_CLASS_NAME;
RegisterClassExW(&wcex);
m_window = CreateWindowExW(WS_EX_TOOLWINDOW, NonLocalizable::TOOL_WINDOW_CLASS_NAME, L"", WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, m_hinstance, this);
if (!m_window)
{
Logger::error(L"Failed to create AlwaysOnTop window: {}", get_last_error_or_default(GetLastError()));
return false;
}
return true;
}
void AlwaysOnTop::SettingsUpdate(SettingId id)
{
switch (id)
{
case SettingId::Hotkey:
{
RegisterHotkey();
}
break;
case SettingId::FrameEnabled:
{
if (AlwaysOnTopSettings::settings().enableFrame)
{
for (auto& iter : m_topmostWindows)
{
if (!iter.second)
{
AssignBorderTracker(iter.first);
}
}
}
else
{
for (auto& iter : m_topmostWindows)
{
iter.second = nullptr;
}
}
}
break;
case SettingId::ExcludeApps:
{
std::vector<HWND> toErase{};
for (const auto& [window, tracker] : m_topmostWindows)
{
if (isExcluded(window))
{
UnpinTopmostWindow(window);
toErase.push_back(window);
}
}
for (const auto window: toErase)
{
m_topmostWindows.erase(window);
}
}
break;
default:
break;
}
}
LRESULT AlwaysOnTop::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept
{
if (message == WM_HOTKEY)
{
if (HWND fw{ GetForegroundWindow() })
{
ProcessCommand(fw);
}
}
else if (message == WM_PRIV_SETTINGS_CHANGED)
{
AlwaysOnTopSettings::instance().LoadSettings();
}
return 0;
}
void AlwaysOnTop::ProcessCommand(HWND window)
{
bool gameMode = detect_game_mode();
if (AlwaysOnTopSettings::settings().blockInGameMode && gameMode)
{
return;
}
if (isExcluded(window))
{
return;
}
Sound::Type soundType = Sound::Type::Off;
bool topmost = IsTopmost(window);
if (topmost)
{
if (UnpinTopmostWindow(window))
{
auto iter = m_topmostWindows.find(window);
if (iter != m_topmostWindows.end())
{
m_topmostWindows.erase(iter);
}
}
}
else
{
if (PinTopmostWindow(window))
{
soundType = Sound::Type::On;
if (AlwaysOnTopSettings::settings().enableFrame)
{
AssignBorderTracker(window);
}
else
{
m_topmostWindows[window] = nullptr;
}
}
}
if (AlwaysOnTopSettings::settings().enableSound)
{
m_sound.Play(soundType);
}
}
void AlwaysOnTop::StartTrackingTopmostWindows()
{
using result_t = std::vector<HWND>;
result_t result;
auto enumWindows = [](HWND hwnd, LPARAM param) -> BOOL {
if (!IsWindowVisible(hwnd))
{
return TRUE;
}
if (isExcluded(hwnd))
{
return TRUE;
}
auto windowName = GetWindowTextLength(hwnd);
if (windowName > 0)
{
result_t& result = *reinterpret_cast<result_t*>(param);
result.push_back(hwnd);
}
return TRUE;
};
EnumWindows(enumWindows, reinterpret_cast<LPARAM>(&result));
for (HWND window : result)
{
if (IsTopmost(window))
{
if (AlwaysOnTopSettings::settings().enableFrame)
{
AssignBorderTracker(window);
}
else
{
m_topmostWindows[window] = nullptr;
}
}
}
}
bool AlwaysOnTop::AssignBorderTracker(HWND window)
{
auto tracker = std::make_unique<WindowBorder>(window);
if (!tracker->Init(m_hinstance))
{
// Failed to init tracker, reset topmost
UnpinTopmostWindow(window);
return false;
}
if (m_virtualDesktopUtils.IsWindowOnCurrentDesktop(window))
{
tracker->Show();
}
m_topmostWindows[window] = std::move(tracker);
return true;
}
void AlwaysOnTop::RegisterHotkey() const
{
UnregisterHotKey(m_window, static_cast<int>(HotkeyId::Pin));
RegisterHotKey(m_window, static_cast<int>(HotkeyId::Pin), AlwaysOnTopSettings::settings().hotkey.get_modifiers(), AlwaysOnTopSettings::settings().hotkey.get_code());
}
void AlwaysOnTop::SubscribeToEvents()
{
// subscribe to windows events
std::array<DWORD, 5> events_to_subscribe = {
EVENT_OBJECT_LOCATIONCHANGE,
EVENT_SYSTEM_MOVESIZEEND,
EVENT_SYSTEM_SWITCHEND,
EVENT_OBJECT_DESTROY,
EVENT_OBJECT_NAMECHANGE
};
for (const auto event : events_to_subscribe)
{
auto hook = SetWinEventHook(event, event, nullptr, WinHookProc, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
if (hook)
{
m_staticWinEventHooks.emplace_back(hook);
}
else
{
Logger::error(L"Failed to set win event hook");
}
}
}
void AlwaysOnTop::UnpinAll()
{
for (const auto& [topWindow, tracker] : m_topmostWindows)
{
if (!UnpinTopmostWindow(topWindow))
{
Logger::error(L"Unpinning topmost window failed");
}
}
m_topmostWindows.clear();
}
void AlwaysOnTop::CleanUp()
{
UnpinAll();
if (m_window)
{
DestroyWindow(m_window);
m_window = nullptr;
}
UnregisterClass(NonLocalizable::TOOL_WINDOW_CLASS_NAME, reinterpret_cast<HINSTANCE>(&__ImageBase));
}
bool AlwaysOnTop::IsTopmost(HWND window) const noexcept
{
int exStyle = GetWindowLong(window, GWL_EXSTYLE);
return (exStyle & WS_EX_TOPMOST) == WS_EX_TOPMOST;
}
bool AlwaysOnTop::PinTopmostWindow(HWND window) const noexcept
{
return SetWindowPos(window, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
}
bool AlwaysOnTop::UnpinTopmostWindow(HWND window) const noexcept
{
return SetWindowPos(window, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
}
bool AlwaysOnTop::IsTracked(HWND window) const noexcept
{
auto iter = m_topmostWindows.find(window);
return (iter != m_topmostWindows.end());
}
void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
{
if (!AlwaysOnTopSettings::settings().enableFrame)
{
return;
}
switch (data->event)
{
case EVENT_OBJECT_LOCATIONCHANGE:
case EVENT_SYSTEM_MOVESIZEEND:
{
auto iter = m_topmostWindows.find(data->hwnd);
if (iter != m_topmostWindows.end())
{
const auto& tracker = iter->second;
tracker->UpdateBorderPosition();
}
}
break;
case EVENT_OBJECT_DESTROY:
{
auto iter = m_topmostWindows.find(data->hwnd);
if (iter != m_topmostWindows.end())
{
m_topmostWindows.erase(iter);
}
}
break;
case EVENT_SYSTEM_SWITCHEND:
{
auto iter = m_topmostWindows.find(data->hwnd);
if (iter != m_topmostWindows.end())
{
const auto& tracker = iter->second;
tracker->Hide();
}
}
break;
case EVENT_OBJECT_NAMECHANGE:
{
// The accessibility name of the desktop window changes whenever the user
// switches virtual desktops.
if (data->hwnd == GetDesktopWindow())
{
VirtualDesktopSwitchedHandle();
}
}
break;
default:
break;
}
}
void AlwaysOnTop::VirtualDesktopSwitchedHandle()
{
for (const auto& [window, tracker] : m_topmostWindows)
{
if (m_virtualDesktopUtils.IsWindowOnCurrentDesktop(window))
{
tracker->Show();
}
else
{
tracker->Hide();
}
}
}

View File

@@ -0,0 +1,88 @@
#pragma once
#include <map>
#include <Settings.h>
#include <SettingsObserver.h>
#include <Sound.h>
#include <VirtualDesktopUtils.h>
#include <WindowBorder.h>
#include <common/hooks/WinHookEvent.h>
class AlwaysOnTop : public SettingsObserver
{
public:
AlwaysOnTop();
~AlwaysOnTop();
protected:
static LRESULT CALLBACK WndProc_Helper(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept
{
auto thisRef = reinterpret_cast<AlwaysOnTop*>(GetWindowLongPtr(window, GWLP_USERDATA));
if (!thisRef && (message == WM_CREATE))
{
const auto createStruct = reinterpret_cast<LPCREATESTRUCT>(lparam);
thisRef = reinterpret_cast<AlwaysOnTop*>(createStruct->lpCreateParams);
SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(thisRef));
}
return thisRef ? thisRef->WndProc(window, message, wparam, lparam) :
DefWindowProc(window, message, wparam, lparam);
}
private:
// IDs used to register hot keys (keyboard shortcuts).
enum class HotkeyId : int
{
Pin = 1,
};
static inline AlwaysOnTop* s_instance = nullptr;
std::vector<HWINEVENTHOOK> m_staticWinEventHooks{};
Sound m_sound;
VirtualDesktopUtils m_virtualDesktopUtils;
HWND m_window{ nullptr };
HINSTANCE m_hinstance;
std::map<HWND, std::unique_ptr<WindowBorder>> m_topmostWindows{};
LRESULT WndProc(HWND, UINT, WPARAM, LPARAM) noexcept;
void HandleWinHookEvent(WinHookEvent* data) noexcept;
bool InitMainWindow();
void RegisterHotkey() const;
void SubscribeToEvents();
void ProcessCommand(HWND window);
void StartTrackingTopmostWindows();
void UnpinAll();
void CleanUp();
void VirtualDesktopSwitchedHandle();
bool IsTracked(HWND window) const noexcept;
bool IsTopmost(HWND window) const noexcept;
bool PinTopmostWindow(HWND window) const noexcept;
bool UnpinTopmostWindow(HWND window) const noexcept;
bool AssignBorderTracker(HWND window);
virtual void SettingsUpdate(SettingId type) override;
static void CALLBACK WinHookProc(HWINEVENTHOOK winEventHook,
DWORD event,
HWND window,
LONG object,
LONG child,
DWORD eventThread,
DWORD eventTime)
{
WinHookEvent data{ event, window, object, child, eventThread, eventTime };
if (s_instance)
{
s_instance->HandleWinHookEvent(&data);
}
}
};

View File

@@ -0,0 +1,203 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Project configurations -->
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.props')" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<!-- Props that should be disabled while building on CI server -->
<ItemDefinitionGroup Condition="'$(CIBuild)'!='true'">
<ClCompile>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<PrecompiledHeader>Use</PrecompiledHeader>
</ClCompile>
</ItemDefinitionGroup>
<!-- C++ source compile-specific things for all configurations -->
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<WarningLevel>Level3</WarningLevel>
<ConformanceMode>false</ConformanceMode>
<TreatWarningAsError>true</TreatWarningAsError>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
</Link>
<Lib>
<TreatLibWarningAsErrors>true</TreatLibWarningAsErrors>
</Lib>
</ItemDefinitionGroup>
<!-- C++ source compile-specific things for Debug/Release configurations -->
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>MaxSpeed</Optimization>
<SDLCheck>false</SDLCheck>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<!-- Global props -->
<PropertyGroup Label="Globals" Condition="'$(OverrideWindowsTargetPlatformVersion)'!='True'">
<WindowsTargetPlatformVersion>10.0.17134.0</WindowsTargetPlatformVersion>
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}</ProjectGuid>
<RootNamespace>AlwaysOnTop</RootNamespace>
</PropertyGroup>
<!-- Props that are constant for both Debug and Release configurations -->
<PropertyGroup Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v142</PlatformToolset>
<IntDir>$(SolutionDir)$(Platform)\$(Configuration)\obj\$(ProjectName)\</IntDir>
<CharacterSet>Unicode</CharacterSet>
<SpectreMitigation>Spectre</SpectreMitigation>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
<TargetName>PowerToys.$(MSBuildProjectName)</TargetName>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\modules\AlwaysOnTop\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
<TargetName>PowerToys.$(MSBuildProjectName)</TargetName>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\modules\AlwaysOnTop\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>./../;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src\common;$(SolutionDir)src\;./;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>winmm.lib;shcore.lib;shlwapi.lib;DbgHelp.lib;uxtheme.lib;dwmapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>./../;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src\common;$(SolutionDir)src\;./;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>winmm.lib;shcore.lib;shlwapi.lib;DbgHelp.lib;uxtheme.lib;dwmapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="AlwaysOnTop.cpp" />
<ClCompile Include="FrameDrawer.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="Settings.cpp" />
<ClCompile Include="trace.cpp" />
<ClCompile Include="VirtualDesktopUtils.cpp" />
<ClCompile Include="WindowBorder.cpp" />
<ClCompile Include="WinHookEventIDs.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="AlwaysOnTop.h" />
<ClInclude Include="FrameDrawer.h" />
<ClInclude Include="ModuleConstants.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="Settings.h" />
<ClInclude Include="SettingsConstants.h" />
<ClInclude Include="SettingsObserver.h" />
<ClInclude Include="Sound.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="VirtualDesktopUtils.h" />
<ClInclude Include="WindowBorder.h" />
<ClInclude Include="WinHookEventIDs.h" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Display\Display.vcxproj">
<Project>{caba8dfb-823b-4bf2-93ac-3f31984150d9}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SetttingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.211019.2\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.211019.2\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.211019.2\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.211019.2\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="AlwaysOnTop.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="FrameDrawer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WindowBorder.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Settings.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WinHookEventIDs.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="VirtualDesktopUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="AlwaysOnTop.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="FrameDrawer.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WindowBorder.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Settings.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WinHookEventIDs.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ModuleConstants.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Sound.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SettingsObserver.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SettingsConstants.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="VirtualDesktopUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,172 @@
#include "pch.h"
#include "FrameDrawer.h"
std::unique_ptr<FrameDrawer> FrameDrawer::Create(HWND window)
{
auto self = std::make_unique<FrameDrawer>(window);
if (self->Init())
{
return self;
}
return nullptr;
}
FrameDrawer::FrameDrawer(FrameDrawer&& other) :
m_window(other.m_window),
m_renderTarget(std::move(other.m_renderTarget)),
m_sceneRect(std::move(other.m_sceneRect)),
m_renderThread(std::move(m_renderThread))
{
}
FrameDrawer::FrameDrawer(HWND window) :
m_window(window), m_renderTarget(nullptr)
{
}
FrameDrawer::~FrameDrawer()
{
m_abortThread = true;
m_renderThread.join();
if (m_renderTarget)
{
m_renderTarget->Release();
}
}
bool FrameDrawer::Init()
{
RECT clientRect;
// Obtain the size of the drawing area.
if (!GetClientRect(m_window, &clientRect))
{
return false;
}
HRESULT hr;
// Create a Direct2D render target
// We should always use the DPI value of 96 since we're running in DPI aware mode
auto renderTargetProperties = D2D1::RenderTargetProperties(
D2D1_RENDER_TARGET_TYPE_DEFAULT,
D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED),
96.f,
96.f);
auto renderTargetSize = D2D1::SizeU(clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
auto hwndRenderTargetProperties = D2D1::HwndRenderTargetProperties(m_window, renderTargetSize);
hr = GetD2DFactory()->CreateHwndRenderTarget(renderTargetProperties, hwndRenderTargetProperties, &m_renderTarget);
if (!SUCCEEDED(hr))
{
return false;
}
m_renderThread = std::thread([this]() { RenderLoop(); });
return true;
}
void FrameDrawer::Hide()
{
ShowWindow(m_window, SW_HIDE);
}
void FrameDrawer::Show()
{
ShowWindow(m_window, SW_SHOWNA);
}
void FrameDrawer::SetBorderRect(RECT windowRect, COLORREF color, float thickness)
{
std::unique_lock lock(m_mutex);
auto borderColor = ConvertColor(color);
m_sceneRect = DrawableRect{
.rect = ConvertRect(windowRect),
.borderColor = borderColor,
.thickness = thickness
};
}
ID2D1Factory* FrameDrawer::GetD2DFactory()
{
static auto pD2DFactory = [] {
ID2D1Factory* res = nullptr;
D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, &res);
return res;
}();
return pD2DFactory;
}
IDWriteFactory* FrameDrawer::GetWriteFactory()
{
static auto pDWriteFactory = [] {
IUnknown* res = nullptr;
DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), &res);
return reinterpret_cast<IDWriteFactory*>(res);
}();
return pDWriteFactory;
}
D2D1_COLOR_F FrameDrawer::ConvertColor(COLORREF color)
{
return D2D1::ColorF(GetRValue(color) / 255.f,
GetGValue(color) / 255.f,
GetBValue(color) / 255.f,
1.f);
}
D2D1_RECT_F FrameDrawer::ConvertRect(RECT rect)
{
return D2D1::RectF((float)rect.left, (float)rect.top, (float)rect.right, (float)rect.bottom);
}
FrameDrawer::RenderResult FrameDrawer::Render()
{
std::unique_lock lock(m_mutex);
if (!m_renderTarget)
{
return RenderResult::Failed;
}
m_renderTarget->BeginDraw();
// Draw backdrop
m_renderTarget->Clear(D2D1::ColorF(0.f, 0.f, 0.f, 0.f));
ID2D1SolidColorBrush* borderBrush = nullptr;
m_renderTarget->CreateSolidColorBrush(m_sceneRect.borderColor, &borderBrush);
if (borderBrush)
{
m_renderTarget->DrawRectangle(m_sceneRect.rect, borderBrush, m_sceneRect.thickness);
borderBrush->Release();
}
// The lock must be released here, as EndDraw() will wait for vertical sync
lock.unlock();
m_renderTarget->EndDraw();
return RenderResult::Ok;
}
void FrameDrawer::RenderLoop()
{
while (!m_abortThread)
{
auto result = Render();
if (result == RenderResult::Failed)
{
Logger::error("Render failed");
Hide();
m_abortThread = true;
}
}
}

View File

@@ -0,0 +1,51 @@
#pragma once
#include <mutex>
#include <d2d1.h>
#include <dwrite.h>
class FrameDrawer
{
public:
static std::unique_ptr<FrameDrawer> Create(HWND window);
FrameDrawer(HWND window);
FrameDrawer(FrameDrawer&& other);
~FrameDrawer();
bool Init();
void Show();
void Hide();
void SetBorderRect(RECT windowRect, COLORREF color, float thickness);
private:
struct DrawableRect
{
D2D1_RECT_F rect;
D2D1_COLOR_F borderColor;
float thickness;
};
enum struct RenderResult
{
Ok,
Failed,
};
static ID2D1Factory* GetD2DFactory();
static IDWriteFactory* GetWriteFactory();
static D2D1_COLOR_F ConvertColor(COLORREF color);
static D2D1_RECT_F ConvertRect(RECT rect);
RenderResult Render();
void RenderLoop();
HWND m_window = nullptr;
ID2D1HwndRenderTarget* m_renderTarget = nullptr;
std::mutex m_mutex;
DrawableRect m_sceneRect;
std::atomic<bool> m_abortThread = false;
std::thread m_renderThread;
};

View File

@@ -0,0 +1,6 @@
#pragma once
namespace NonLocalizable
{
const inline wchar_t ModuleKey[] = L"AlwaysOnTop";
}

View File

@@ -0,0 +1,188 @@
#include "pch.h"
#include "Settings.h"
#include <ModuleConstants.h>
#include <SettingsObserver.h>
#include <WinHookEventIDs.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/utils/string_utils.h> // trim
namespace NonLocalizable
{
const static wchar_t* SettingsFileName = L"settings.json";
const static wchar_t* HotkeyID = L"hotkey";
const static wchar_t* SoundEnabledID = L"sound-enabled";
const static wchar_t* FrameEnabledID = L"frame-enabled";
const static wchar_t* FrameThicknessID = L"frame-thickness";
const static wchar_t* FrameColorID = L"frame-color";
const static wchar_t* BlockInGameModeID = L"do-not-activate-on-game-mode";
const static wchar_t* ExcludedAppsID = L"excluded-apps";
}
// TODO: move to common utils
inline COLORREF HexToRGB(std::wstring_view hex, const COLORREF fallbackColor = RGB(255, 255, 255))
{
hex = left_trim<wchar_t>(trim<wchar_t>(hex), L"#");
try
{
const long long tmp = std::stoll(hex.data(), nullptr, 16);
const BYTE nR = static_cast<BYTE>((tmp & 0xFF0000) >> 16);
const BYTE nG = static_cast<BYTE>((tmp & 0xFF00) >> 8);
const BYTE nB = static_cast<BYTE>((tmp & 0xFF));
return RGB(nR, nG, nB);
}
catch (const std::exception&)
{
return fallbackColor;
}
}
AlwaysOnTopSettings::AlwaysOnTopSettings()
{
}
AlwaysOnTopSettings& AlwaysOnTopSettings::instance()
{
static AlwaysOnTopSettings instance;
return instance;
}
void AlwaysOnTopSettings::InitFileWatcher()
{
const std::wstring& settingsFileName = GetSettingsFileName();
m_settingsFileWatcher = std::make_unique<FileWatcher>(settingsFileName, [&]() {
PostMessageW(HWND_BROADCAST, WM_PRIV_SETTINGS_CHANGED, NULL, NULL);
});
}
std::wstring AlwaysOnTopSettings::GetSettingsFileName()
{
std::wstring saveFolderPath = PTSettingsHelper::get_module_save_folder_location(NonLocalizable::ModuleKey);
return saveFolderPath + L"\\" + std::wstring(NonLocalizable::SettingsFileName);
}
void AlwaysOnTopSettings::AddObserver(SettingsObserver& observer)
{
m_observers.insert(&observer);
}
void AlwaysOnTopSettings::RemoveObserver(SettingsObserver& observer)
{
auto iter = m_observers.find(&observer);
if (iter != m_observers.end())
{
m_observers.erase(iter);
}
}
void AlwaysOnTopSettings::LoadSettings()
{
try
{
PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(NonLocalizable::ModuleKey);
if (const auto jsonVal = values.get_json(NonLocalizable::HotkeyID))
{
auto val = PowerToysSettings::HotkeyObject::from_json(*jsonVal);
if (m_settings.hotkey.get_modifiers() != val.get_modifiers() || m_settings.hotkey.get_key() != val.get_key() || m_settings.hotkey.get_code() != val.get_code())
{
m_settings.hotkey = val;
NotifyObservers(SettingId::Hotkey);
}
}
if (const auto jsonVal = values.get_bool_value(NonLocalizable::SoundEnabledID))
{
auto val = *jsonVal;
if (m_settings.enableSound != val)
{
m_settings.enableSound = val;
NotifyObservers(SettingId::SoundEnabled);
}
}
if (const auto jsonVal = values.get_int_value(NonLocalizable::FrameThicknessID))
{
auto val = *jsonVal;
if (m_settings.frameThickness != val)
{
m_settings.frameThickness = static_cast<float>(val);
NotifyObservers(SettingId::FrameThickness);
}
}
if (const auto jsonVal = values.get_string_value(NonLocalizable::FrameColorID))
{
auto val = HexToRGB(*jsonVal);
if (m_settings.frameColor != val)
{
m_settings.frameColor = val;
NotifyObservers(SettingId::FrameColor);
}
}
if (const auto jsonVal = values.get_bool_value(NonLocalizable::FrameEnabledID))
{
auto val = *jsonVal;
if (m_settings.enableFrame != val)
{
m_settings.enableFrame = val;
NotifyObservers(SettingId::FrameEnabled);
}
}
if (const auto jsonVal = values.get_bool_value(NonLocalizable::BlockInGameModeID))
{
auto val = *jsonVal;
if (m_settings.blockInGameMode != val)
{
m_settings.blockInGameMode = val;
NotifyObservers(SettingId::BlockInGameMode);
}
}
if (auto jsonVal = values.get_string_value(NonLocalizable::ExcludedAppsID))
{
std::wstring apps = std::move(*jsonVal);
std::vector<std::wstring> excludedApps;
auto excludedUppercase = apps;
CharUpperBuffW(excludedUppercase.data(), (DWORD)excludedUppercase.length());
std::wstring_view view(excludedUppercase);
view = left_trim<wchar_t>(trim<wchar_t>(view));
while (!view.empty())
{
auto pos = (std::min)(view.find_first_of(L"\r\n"), view.length());
excludedApps.emplace_back(view.substr(0, pos));
view.remove_prefix(pos);
view = left_trim<wchar_t>(trim<wchar_t>(view));
}
if (m_settings.excludedApps != excludedApps)
{
m_settings.excludedApps = excludedApps;
NotifyObservers(SettingId::ExcludeApps);
}
}
}
catch (...)
{
// Log error message and continue with default settings.
Logger::error("Failed to read settings");
// TODO: show localized message
}
}
void AlwaysOnTopSettings::NotifyObservers(SettingId id) const
{
for (auto observer : m_observers)
{
if (observer->WantsToBeNotified(id))
{
observer->SettingsUpdate(id);
}
}
}

View File

@@ -0,0 +1,50 @@
#pragma once
#include <unordered_set>
#include <common/SettingsAPI/FileWatcher.h>
#include <common/SettingsAPI/settings_objects.h>
#include <SettingsConstants.h>
class SettingsObserver;
// Needs to be kept in sync with src\settings-ui\Settings.UI.Library\AlwaysOnTopProperties.cs
struct Settings
{
PowerToysSettings::HotkeyObject hotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, 84); // win + ctrl + T
bool enableFrame = true;
bool enableSound = true;
bool blockInGameMode = true;
float frameThickness = 15.0f;
COLORREF frameColor = RGB(0, 173, 239);
std::vector<std::wstring> excludedApps{};
};
class AlwaysOnTopSettings
{
public:
static AlwaysOnTopSettings& instance();
static inline const Settings& settings()
{
return instance().m_settings;
}
void InitFileWatcher();
static std::wstring GetSettingsFileName();
void AddObserver(SettingsObserver& observer);
void RemoveObserver(SettingsObserver& observer);
void LoadSettings();
private:
AlwaysOnTopSettings();
~AlwaysOnTopSettings() = default;
Settings m_settings;
std::unique_ptr<FileWatcher> m_settingsFileWatcher;
std::unordered_set<SettingsObserver*> m_observers;
void NotifyObservers(SettingId id) const;
};

View File

@@ -0,0 +1,12 @@
#pragma once
enum class SettingId
{
Hotkey = 0,
SoundEnabled,
FrameEnabled,
FrameThickness,
FrameColor,
BlockInGameMode,
ExcludeApps
};

View File

@@ -0,0 +1,31 @@
#pragma once
#include <unordered_set>
#include <Settings.h>
#include <SettingsConstants.h>
class SettingsObserver
{
public:
SettingsObserver(std::unordered_set<SettingId> observedSettings) :
m_observedSettings(std::move(observedSettings))
{
AlwaysOnTopSettings::instance().AddObserver(*this);
}
virtual ~SettingsObserver()
{
AlwaysOnTopSettings::instance().RemoveObserver(*this);
}
virtual void SettingsUpdate(SettingId type) {}
bool WantsToBeNotified(SettingId type) const noexcept
{
return m_observedSettings.contains(type);
}
protected:
std::unordered_set<SettingId> m_observedSettings;
};

View File

@@ -0,0 +1,44 @@
#pragma once
#include "pch.h"
#include <atomic>
#include <mmsystem.h> // sound
class Sound
{
public:
enum class Type
{
On,
Off,
};
Sound()
: isPlaying(false)
{}
void Play(Type type)
{
BOOL success = false;
switch (type)
{
case Type::On:
success = PlaySound(TEXT("Media\\Speech On.wav"), NULL, SND_FILENAME | SND_ASYNC);
break;
case Type::Off:
success = PlaySound(TEXT("Media\\Speech Off.wav"), NULL, SND_FILENAME | SND_ASYNC);
break;
default:
break;
}
if (!success)
{
Logger::error(L"Sound playing error");
}
}
private:
std::atomic<bool> isPlaying;
};

View File

@@ -0,0 +1,63 @@
#include "pch.h"
#include "VirtualDesktopUtils.h"
// Non-Localizable strings
namespace NonLocalizable
{
const wchar_t RegKeyVirtualDesktops[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\VirtualDesktops";
}
HKEY OpenVirtualDesktopsRegKey()
{
HKEY hKey{ nullptr };
if (RegOpenKeyEx(HKEY_CURRENT_USER, NonLocalizable::RegKeyVirtualDesktops, 0, KEY_ALL_ACCESS, &hKey) == ERROR_SUCCESS)
{
return hKey;
}
return nullptr;
}
HKEY GetVirtualDesktopsRegKey()
{
static wil::unique_hkey virtualDesktopsKey{ OpenVirtualDesktopsRegKey() };
return virtualDesktopsKey.get();
}
VirtualDesktopUtils::VirtualDesktopUtils()
{
auto res = CoCreateInstance(CLSID_VirtualDesktopManager, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&m_vdManager));
if (FAILED(res))
{
Logger::error("Failed to create VirtualDesktopManager instance");
}
}
VirtualDesktopUtils::~VirtualDesktopUtils()
{
if (m_vdManager)
{
m_vdManager->Release();
}
}
bool VirtualDesktopUtils::IsWindowOnCurrentDesktop(HWND window) const
{
std::optional<GUID> id = GetDesktopId(window);
return id.has_value();
}
std::optional<GUID> VirtualDesktopUtils::GetDesktopId(HWND window) const
{
GUID id;
BOOL isWindowOnCurrentDesktop = false;
if (m_vdManager && m_vdManager->IsWindowOnCurrentVirtualDesktop(window, &isWindowOnCurrentDesktop) == S_OK && isWindowOnCurrentDesktop)
{
// Filter windows such as Windows Start Menu, Task Switcher, etc.
if (m_vdManager->GetWindowDesktopId(window, &id) == S_OK && id != GUID_NULL)
{
return id;
}
}
return std::nullopt;
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include <ShObjIdl.h>
class VirtualDesktopUtils
{
public:
VirtualDesktopUtils();
~VirtualDesktopUtils();
bool IsWindowOnCurrentDesktop(HWND window) const;
std::optional<GUID> GetDesktopId(HWND window) const;
private:
IVirtualDesktopManager* m_vdManager;
};

View File

@@ -0,0 +1,14 @@
#include "pch.h"
#include "WinHookEventIDs.h"
UINT WM_PRIV_SETTINGS_CHANGED;
std::once_flag init_flag;
void InitializeWinhookEventIds()
{
std::call_once(init_flag, [&] {
WM_PRIV_SETTINGS_CHANGED = RegisterWindowMessage(L"{11978F7B-221A-4E65-B8A8-693F7D6E4B25}");
});
}

View File

@@ -0,0 +1,5 @@
#pragma once
extern UINT WM_PRIV_SETTINGS_CHANGED; // Scheduled when the a watched settings file is updated
void InitializeWinhookEventIds();

View File

@@ -0,0 +1,223 @@
#include "pch.h"
#include "WindowBorder.h"
#include <dwmapi.h>
#include <FrameDrawer.h>
#include <Settings.h>
// Non-Localizable strings
namespace NonLocalizable
{
const wchar_t ToolWindowClassName[] = L"AlwaysOnTop_Border";
}
std::optional<RECT> GetFrameRect(HWND window)
{
RECT rect;
if (!SUCCEEDED(DwmGetWindowAttribute(window, DWMWA_EXTENDED_FRAME_BOUNDS, &rect, sizeof(rect))))
{
return std::nullopt;
}
int border = static_cast<int>(AlwaysOnTopSettings::settings().frameThickness / 2);
rect.top -= border;
rect.left -= border;
rect.right += border;
rect.bottom += border;
return rect;
}
WindowBorder::WindowBorder(HWND window) :
SettingsObserver({SettingId::FrameColor, SettingId::FrameThickness}),
m_window(nullptr),
m_trackingWindow(window),
m_frameDrawer(nullptr)
{
}
WindowBorder::WindowBorder(WindowBorder&& other) :
SettingsObserver({ SettingId::FrameColor, SettingId::FrameThickness }),
m_window(other.m_window),
m_trackingWindow(other.m_trackingWindow),
m_frameDrawer(std::move(other.m_frameDrawer))
{
}
WindowBorder::~WindowBorder()
{
if (m_frameDrawer)
{
m_frameDrawer->Hide();
m_frameDrawer = nullptr;
}
if (m_window)
{
SetWindowLongPtrW(m_window, GWLP_USERDATA, 0);
ShowWindow(m_window, SW_HIDE);
}
}
bool WindowBorder::Init(HINSTANCE hinstance)
{
if (!m_trackingWindow)
{
return false;
}
auto windowRectOpt = GetFrameRect(m_trackingWindow);
if (!windowRectOpt.has_value())
{
return false;
}
RECT windowRect = windowRectOpt.value();
WNDCLASSEXW wcex{};
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.lpfnWndProc = s_WndProc;
wcex.hInstance = hinstance;
wcex.lpszClassName = NonLocalizable::ToolWindowClassName;
wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW);
RegisterClassExW(&wcex);
m_window = CreateWindowExW(WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW
, NonLocalizable::ToolWindowClassName
, L""
, WS_POPUP
, windowRect.left
, windowRect.top
, windowRect.right - windowRect.left
, windowRect.bottom - windowRect.top
, nullptr
, nullptr
, hinstance
, this);
if (!m_window)
{
return false;
}
if (!SetLayeredWindowAttributes(m_window, RGB(0, 0, 0), 0, LWA_COLORKEY))
{
return false;
}
// set position of the border-window behind the tracking window
// helps to prevent border overlapping (happens after turning borders off and on)
SetWindowPos(m_trackingWindow
, m_window
, windowRect.left
, windowRect.top
, windowRect.right - windowRect.left - static_cast<int>(AlwaysOnTopSettings::settings().frameThickness)
, windowRect.bottom - windowRect.top - static_cast<int>(AlwaysOnTopSettings::settings().frameThickness)
, SWP_NOMOVE | SWP_NOSIZE);
m_frameDrawer = FrameDrawer::Create(m_window);
return m_frameDrawer != nullptr;
}
void WindowBorder::UpdateBorderPosition() const
{
if (!m_trackingWindow)
{
return;
}
auto rectOpt = GetFrameRect(m_trackingWindow);
if (!rectOpt.has_value())
{
return;
}
RECT rect = rectOpt.value();
SetWindowPos(m_window, m_trackingWindow, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOREDRAW);
}
void WindowBorder::UpdateBorderProperties() const
{
if (!m_trackingWindow || !m_frameDrawer)
{
return;
}
auto windowRectOpt = GetFrameRect(m_trackingWindow);
if (!windowRectOpt.has_value())
{
return;
}
RECT windowRect = windowRectOpt.value();
RECT frameRect{ 0, 0, windowRect.right - windowRect.left, windowRect.bottom - windowRect.top };
m_frameDrawer->SetBorderRect(frameRect, AlwaysOnTopSettings::settings().frameColor, AlwaysOnTopSettings::settings().frameThickness);
}
void WindowBorder::Show() const
{
UpdateBorderProperties();
m_frameDrawer->Show();
}
void WindowBorder::Hide() const
{
m_frameDrawer->Hide();
}
LRESULT WindowBorder::WndProc(UINT message, WPARAM wparam, LPARAM lparam) noexcept
{
switch (message)
{
case WM_NCDESTROY:
{
::DefWindowProc(m_window, message, wparam, lparam);
SetWindowLongPtr(m_window, GWLP_USERDATA, 0);
}
break;
case WM_ERASEBKGND:
return TRUE;
default:
{
return DefWindowProc(m_window, message, wparam, lparam);
}
}
return FALSE;
}
void WindowBorder::SettingsUpdate(SettingId id)
{
if (!AlwaysOnTopSettings::settings().enableFrame)
{
return;
}
auto windowRectOpt = GetFrameRect(m_trackingWindow);
if (!windowRectOpt.has_value())
{
return;
}
switch (id)
{
case SettingId::FrameThickness:
{
UpdateBorderPosition();
UpdateBorderProperties();
}
break;
case SettingId::FrameColor:
{
UpdateBorderProperties();
}
break;
default:
break;
}
}

View File

@@ -0,0 +1,45 @@
#pragma once
#include <SettingsObserver.h>
class FrameDrawer;
class WindowBorder : public SettingsObserver
{
public:
WindowBorder(HWND window);
WindowBorder(WindowBorder&& other);
~WindowBorder();
bool Init(HINSTANCE hinstance);
void Show() const;
void Hide() const;
void UpdateBorderPosition() const;
void UpdateBorderProperties() const;
protected:
static LRESULT CALLBACK s_WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept
{
auto thisRef = reinterpret_cast<WindowBorder*>(GetWindowLongPtr(window, GWLP_USERDATA));
if ((thisRef == nullptr) && (message == WM_CREATE))
{
auto createStruct = reinterpret_cast<LPCREATESTRUCT>(lparam);
thisRef = reinterpret_cast<WindowBorder*>(createStruct->lpCreateParams);
SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(thisRef));
}
return (thisRef != nullptr) ? thisRef->WndProc(message, wparam, lparam) :
DefWindowProc(window, message, wparam, lparam);
}
private:
HWND m_window;
HWND m_trackingWindow;
std::unique_ptr<FrameDrawer> m_frameDrawer;
LRESULT WndProc(UINT message, WPARAM wparam, LPARAM lparam) noexcept;
virtual void SettingsUpdate(SettingId id) override;
};

View File

@@ -0,0 +1,62 @@
#include "pch.h"
#include <common/utils/ProcessWaiter.h>
#include <common/utils/window.h>
#include <common/utils/UnhandledExceptionHandler_x64.h>
#include <common/utils/logger_helper.h>
#include <AlwaysOnTop.h>
#include <trace.h>
// Non-localizable
const std::wstring moduleName = L"AlwaysOnTop";
const std::wstring internalPath = L"";
const std::wstring instanceMutexName = L"Local\\PowerToys_AlwaysOnTop_InstanceMutex";
int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ PWSTR lpCmdLine, _In_ int nCmdShow)
{
winrt::init_apartment();
LoggerHelpers::init_logger(moduleName, internalPath, LogSettings::alwaysOnTopLoggerName);
InitUnhandledExceptionHandler_x64();
auto mutex = CreateMutex(nullptr, true, instanceMutexName.c_str());
if (mutex == nullptr)
{
Logger::error(L"Failed to create mutex. {}", get_last_error_or_default(GetLastError()));
}
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
return 0;
}
std::wstring pid = std::wstring(lpCmdLine);
if (!pid.empty())
{
auto mainThreadId = GetCurrentThreadId();
ProcessWaiter::OnProcessTerminate(pid, [mainThreadId](int err) {
if (err != ERROR_SUCCESS)
{
Logger::error(L"Failed to wait for parent process exit. {}", get_last_error_or_default(err));
}
else
{
Logger::trace(L"PowerToys runner exited.");
}
Logger::trace(L"Exiting AlwaysOnTop");
PostThreadMessage(mainThreadId, WM_QUIT, 0, 0);
});
}
Trace::RegisterProvider();
AlwaysOnTop app;
run_message_loop();
Trace::UnregisterProvider();
return 0;
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.200729.8" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.211019.2" targetFramework="native" />
</packages>

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,7 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winrt/base.h>
#include <wil/resource.h>
#include <ProjectTelemetry.h>
#include <common/logger/logger.h>

View File

@@ -0,0 +1,35 @@
#include "pch.h"
#include "trace.h"
// Telemetry strings should not be localized.
#define LoggingProviderKey "Microsoft.PowerToys"
#define EventEnableAlwaysOnTopKey "AlwaysOnTop_EnableAlwaysOnTop"
#define EventEnabledKey "Enabled"
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
LoggingProviderKey,
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
void Trace::RegisterProvider() noexcept
{
TraceLoggingRegister(g_hProvider);
}
void Trace::UnregisterProvider() noexcept
{
TraceLoggingUnregister(g_hProvider);
}
void Trace::AlwaysOnTop::Enable(bool enabled) noexcept
{
TraceLoggingWrite(
g_hProvider,
EventEnableAlwaysOnTopKey,
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, EventEnabledKey));
}

View File

@@ -0,0 +1,14 @@
#pragma once
class Trace
{
public:
static void RegisterProvider() noexcept;
static void UnregisterProvider() noexcept;
class AlwaysOnTop
{
public:
static void Enable(bool enabled) noexcept;
};
};

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>alwaysontop</RootNamespace>
<ProjectName>AlwaysOnTopModuleInterface</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.210204.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.210204.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\modules\AlwaysOnTop\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<TargetName>PowerToys.AlwaysOnTopModuleInterface</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<TargetName>PowerToys.AlwaysOnTopModuleInterface</TargetName>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions>_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>..\;..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<AdditionalDependencies>gdiplus.lib;dwmapi.lib;shlwapi.lib;uxtheme.lib;shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="..\AlwaysOnTop\trace.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="targetver.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\AlwaysOnTop\trace.cpp" />
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(CIBuild)'!='true'">Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SetttingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200729.8\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.210204.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.210204.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="dllmain.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\AlwaysOnTop\trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="targetver.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\AlwaysOnTop\trace.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{21926bf1-03b3-482d-8f60-8bc4fbfc6564}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{2f10207d-d8d1-4a42-8027-8ca597b3cb23}</UniqueIdentifier>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{a4241930-ecae-44e2-be82-25eff2499fcd}</UniqueIdentifier>
</Filter>
<Filter Include="Generated Files">
<UniqueIdentifier>{8d479404-964b-4eb1-8fe8-554be3e68c9b}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,159 @@
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/logger/logger.h>
#include <common/utils/resources.h>
#include <common/utils/winapi_error.h>
#include <AlwaysOnTop/trace.h>
#include <AlwaysOnTop/ModuleConstants.h>
#include <shellapi.h>
namespace NonLocalizable
{
const wchar_t ModulePath[] = L"modules\\AlwaysOnTop\\PowerToys.AlwaysOnTop.exe";
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
return TRUE;
}
class AlwaysOnTopModuleInterface : public PowertoyModuleIface
{
public:
// Return the localized display name of the powertoy
virtual PCWSTR get_name() override
{
return app_name.c_str();
}
// Return the non localized key of the powertoy, this will be cached by the runner
virtual const wchar_t* get_key() override
{
return app_key.c_str();
}
// Return JSON with the configuration options.
// These are the settings shown on the settings page along with their current values.
virtual bool get_config(_Out_ PWSTR buffer, _Out_ int* buffer_size) override
{
return false;
}
// Passes JSON with the configuration settings for the powertoy.
// This is called when the user hits Save on the settings page.
virtual void set_config(PCWSTR config) override
{
}
// Enable the powertoy
virtual void enable()
{
Logger::info("AlwaysOnTop enabling");
Enable();
}
// Disable the powertoy
virtual void disable()
{
Logger::info("AlwaysOnTop disabling");
Disable(true);
}
// Returns if the powertoy is enabled
virtual bool is_enabled() override
{
return m_enabled;
}
// Destroy the powertoy and free memory
virtual void destroy() override
{
Disable(false);
delete this;
}
AlwaysOnTopModuleInterface()
{
app_name = L"AlwaysOnTop"; //TODO: localize
app_key = NonLocalizable::ModuleKey;
}
private:
void Enable()
{
m_enabled = true;
// Log telemetry
Trace::AlwaysOnTop::Enable(true);
unsigned long powertoys_pid = GetCurrentProcessId();
std::wstring executable_args = L"";
executable_args.append(std::to_wstring(powertoys_pid));
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
sei.lpFile = NonLocalizable::ModulePath;
sei.nShow = SW_SHOWNORMAL;
sei.lpParameters = executable_args.data();
if (ShellExecuteExW(&sei) == false)
{
Logger::error(L"Failed to start AlwaysOnTop");
auto message = get_last_error_message(GetLastError());
if (message.has_value())
{
Logger::error(message.value());
}
}
else
{
m_hProcess = sei.hProcess;
}
}
void Disable(bool const traceEvent)
{
m_enabled = false;
// Log telemetry
if (traceEvent)
{
Trace::AlwaysOnTop::Enable(false);
}
if (m_hProcess)
{
TerminateProcess(m_hProcess, 0);
m_hProcess = nullptr;
}
}
std::wstring app_name;
std::wstring app_key; //contains the non localized key of the powertoy
bool m_enabled = false;
HANDLE m_hProcess = nullptr;
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new AlwaysOnTopModuleInterface();
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.200729.8" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.210204.1" targetFramework="native" />
</packages>

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,17 @@
#pragma once
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#include <Unknwn.h>
#include <winrt/base.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <ProjectTelemetry.h>
#include <TraceLoggingActivity.h>
#include <wil\common.h>
#include <wil\result.h>
namespace winrt
{
using namespace ::winrt;
}

View File

@@ -149,7 +149,8 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
L"modules/ColorPicker/PowerToys.ColorPicker.dll",
L"modules/Awake/PowerToys.AwakeModuleInterface.dll",
L"modules/MouseUtils/PowerToys.FindMyMouse.dll" ,
L"modules/MouseUtils/PowerToys.MouseHighlighter.dll"
L"modules/MouseUtils/PowerToys.MouseHighlighter.dll",
L"modules/AlwaysOnTop/PowerToys.AlwaysOnTopModuleInterface.dll",
};
const auto VCM_PATH = L"modules/VideoConference/PowerToys.VideoConferenceModule.dll";

View File

@@ -62,6 +62,7 @@ namespace PowerToys.Settings
switch (args[(int)Arguments.SettingsWindow])
{
case "Overview": app.StartupPage = typeof(Microsoft.PowerToys.Settings.UI.Views.GeneralPage); break;
case "AlwaysOnTop": app.StartupPage = typeof(Microsoft.PowerToys.Settings.UI.Views.AlwaysOnTopPage); break;
case "Awake": app.StartupPage = typeof(Microsoft.PowerToys.Settings.UI.Views.AwakePage); break;
case "ColorPicker": app.StartupPage = typeof(Microsoft.PowerToys.Settings.UI.Views.ColorPickerPage); break;
case "FancyZones": app.StartupPage = typeof(Microsoft.PowerToys.Settings.UI.Views.FancyZonesPage); break;

View File

@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
// Needs to be kept in sync with src\modules\alwaysontop\AlwaysOnTop\Settings.h
public class AlwaysOnTopProperties
{
public static readonly HotkeySettings DefaultHotkeyValue = new HotkeySettings(true, true, false, false, 0x54);
public const bool DefaultFrameEnabled = true;
public const int DefaultFrameThickness = 15;
public const string DefaultFrameColor = "#0099cc";
public const bool DefaultSoundEnabled = true;
public const bool DefaultDoNotActivateOnGameMode = true;
public AlwaysOnTopProperties()
{
Hotkey = new KeyboardKeysProperty(DefaultHotkeyValue);
FrameEnabled = new BoolProperty(DefaultFrameEnabled);
FrameThickness = new IntProperty(DefaultFrameThickness);
FrameColor = new StringProperty(DefaultFrameColor);
SoundEnabled = new BoolProperty(DefaultSoundEnabled);
DoNotActivateOnGameMode = new BoolProperty(DefaultDoNotActivateOnGameMode);
ExcludedApps = new StringProperty();
}
[JsonPropertyName("hotkey")]
public KeyboardKeysProperty Hotkey { get; set; }
[JsonPropertyName("frame-enabled")]
public BoolProperty FrameEnabled { get; set; }
[JsonPropertyName("frame-thickness")]
public IntProperty FrameThickness { get; set; }
[JsonPropertyName("frame-color")]
public StringProperty FrameColor { get; set; }
[JsonPropertyName("sound-enabled")]
public BoolProperty SoundEnabled { get; set; }
[JsonPropertyName("do-not-activate-on-game-mode")]
public BoolProperty DoNotActivateOnGameMode { get; set; }
[JsonPropertyName("excluded-apps")]
public StringProperty ExcludedApps { get; set; }
public string ToJsonString()
{
return JsonSerializer.Serialize(this);
}
}
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class AlwaysOnTopSettings : BasePTModuleSettings, ISettingsConfig
{
public const string ModuleName = "AlwaysOnTop";
public const string ModuleVersion = "0.0.1";
public AlwaysOnTopSettings()
{
Name = ModuleName;
Version = ModuleVersion;
Properties = new AlwaysOnTopProperties();
}
[JsonPropertyName("properties")]
public AlwaysOnTopProperties Properties { get; set; }
public string GetModuleName()
{
return Name;
}
public bool UpgradeSettingsConfiguration()
{
return false;
}
}
}

View File

@@ -207,6 +207,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private bool alwaysOnTop = true;
[JsonPropertyName("AlwaysOnTop")]
public bool AlwaysOnTop
{
get => alwaysOnTop;
set
{
if (alwaysOnTop != value)
{
LogTelemetryEvent(value);
alwaysOnTop = value;
}
}
}
public string ToJsonString()
{
return JsonSerializer.Serialize(this);

View File

@@ -0,0 +1,208 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.CompilerServices;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels
{
public class AlwaysOnTopViewModel : Observable
{
private ISettingsUtils SettingsUtils { get; set; }
private GeneralSettings GeneralSettingsConfig { get; set; }
private AlwaysOnTopSettings Settings { get; set; }
private Func<string, int> SendConfigMSG { get; }
public AlwaysOnTopViewModel(ISettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<AlwaysOnTopSettings> moduleSettingsRepository, Func<string, int> ipcMSGCallBackFunc)
{
if (settingsUtils == null)
{
throw new ArgumentNullException(nameof(settingsUtils));
}
SettingsUtils = settingsUtils;
// To obtain the general settings configurations of PowerToys Settings.
if (settingsRepository == null)
{
throw new ArgumentNullException(nameof(settingsRepository));
}
GeneralSettingsConfig = settingsRepository.SettingsConfig;
// To obtain the settings configurations of AlwaysOnTop.
if (moduleSettingsRepository == null)
{
throw new ArgumentNullException(nameof(moduleSettingsRepository));
}
Settings = moduleSettingsRepository.SettingsConfig;
_isEnabled = GeneralSettingsConfig.Enabled.AlwaysOnTop;
_hotkey = Settings.Properties.Hotkey.Value;
_frameEnabled = Settings.Properties.FrameEnabled.Value;
_frameThickness = Settings.Properties.FrameThickness.Value;
_frameColor = Settings.Properties.FrameColor.Value;
_soundEnabled = Settings.Properties.SoundEnabled.Value;
_doNotActivateOnGameMode = Settings.Properties.DoNotActivateOnGameMode.Value;
_excludedApps = Settings.Properties.ExcludedApps.Value;
// set the callback functions value to hangle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
}
public bool IsEnabled
{
get => _isEnabled;
set
{
if (value != _isEnabled)
{
_isEnabled = value;
// Set the status in the general settings configuration
GeneralSettingsConfig.Enabled.AlwaysOnTop = value;
OutGoingGeneralSettings snd = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(snd.ToString());
OnPropertyChanged(nameof(IsEnabled));
}
}
}
public HotkeySettings Hotkey
{
get => _hotkey;
set
{
if (value != _hotkey)
{
if (value == null || value.IsEmpty())
{
_hotkey = AlwaysOnTopProperties.DefaultHotkeyValue;
}
else
{
_hotkey = value;
}
Settings.Properties.Hotkey.Value = _hotkey;
NotifyPropertyChanged();
}
}
}
public bool FrameEnabled
{
get => _frameEnabled;
set
{
if (value != _frameEnabled)
{
_frameEnabled = value;
Settings.Properties.FrameEnabled.Value = value;
NotifyPropertyChanged();
}
}
}
public int FrameThickness
{
get => _frameThickness;
set
{
if (value != _frameThickness)
{
_frameThickness = value;
Settings.Properties.FrameThickness.Value = value;
NotifyPropertyChanged();
}
}
}
public string FrameColor
{
get => _frameColor;
set
{
if (value != _frameColor)
{
_frameColor = value;
Settings.Properties.FrameColor.Value = value;
NotifyPropertyChanged();
}
}
}
public bool SoundEnabled
{
get => _soundEnabled;
set
{
if (value != _soundEnabled)
{
_soundEnabled = value;
Settings.Properties.SoundEnabled.Value = value;
NotifyPropertyChanged();
}
}
}
public bool DoNotActivateOnGameMode
{
get => _doNotActivateOnGameMode;
set
{
if (value != _doNotActivateOnGameMode)
{
_doNotActivateOnGameMode = value;
Settings.Properties.DoNotActivateOnGameMode.Value = value;
NotifyPropertyChanged();
}
}
}
public string ExcludedApps
{
get => _excludedApps;
set
{
if (value != _excludedApps)
{
_excludedApps = value;
Settings.Properties.ExcludedApps.Value = value;
NotifyPropertyChanged();
}
}
}
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(propertyName);
SettingsUtils.SaveSettings(Settings.ToJsonString(), AlwaysOnTopSettings.ModuleName);
}
private bool _isEnabled;
private HotkeySettings _hotkey;
private bool _frameEnabled;
private int _frameThickness;
private string _frameColor;
private bool _soundEnabled;
private bool _doNotActivateOnGameMode;
private string _excludedApps;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -7,6 +7,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
public enum PowerToysModulesEnum
{
Overview = 0,
AlwaysOnTop,
Awake,
ColorPicker,
FancyZones,

View File

@@ -0,0 +1,41 @@
<Page
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.OobeAlwaysOnTop"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.OOBE.Views"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:toolkitcontrols="using:Microsoft.Toolkit.Uwp.UI.Controls">
<controls:OOBEPageControl ModuleTitle="{x:Bind ViewModel.ModuleName}"
ModuleImageSource="{x:Bind ViewModel.PreviewImageSource}"
ModuleDescription="{x:Bind ViewModel.Description}">
<controls:OOBEPageControl.ModuleContent>
<StackPanel Orientation="Vertical">
<TextBlock x:Uid="Oobe_HowToUse"
Style="{ThemeResource OobeSubtitleStyle}" />
<controls:ShortcutWithTextLabelControl x:Name="HotkeyControl" x:Uid="Oobe_AlwaysOnTop_HowToUse" />
<TextBlock x:Uid="Oobe_TipsAndTricks"
Style="{ThemeResource OobeSubtitleStyle}"/>
<toolkitcontrols:MarkdownTextBlock Background="Transparent" x:Uid="Oobe_AlwaysOnTop_TipsAndTricks" />
<StackPanel Orientation="Horizontal" Spacing="12" Margin="0,24,0,0">
<Button x:Uid="OOBE_Settings"
Click="SettingsLaunchButton_Click"/>
<HyperlinkButton NavigateUri="{x:Bind ViewModel.Link}"
Style="{StaticResource TextButtonStyle}">
<TextBlock x:Uid="LearnMore_AlwaysOnTop"
TextWrapping="Wrap" />
</HyperlinkButton>
</StackPanel>
</StackPanel>
</controls:OOBEPageControl.ModuleContent>
</controls:OOBEPageControl>
</Page>

View File

@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.Views;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
public sealed partial class OobeAlwaysOnTop : Page
{
public OobePowerToysModule ViewModel { get; set; }
public OobeAlwaysOnTop()
{
InitializeComponent();
ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModulesEnum.AlwaysOnTop]);
DataContext = ViewModel;
}
private void SettingsLaunchButton_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
if (OobeShellPage.OpenMainWindowCallback != null)
{
OobeShellPage.OpenMainWindowCallback(typeof(AlwaysOnTopPage));
}
ViewModel.LogOpeningSettingsEvent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
ViewModel.LogOpeningModuleEvent();
HotkeyControl.Keys = SettingsRepository<AlwaysOnTopSettings>.GetInstance(new SettingsUtils()).SettingsConfig.Properties.Hotkey.Value.GetKeysList();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
ViewModel.LogClosingModuleEvent();
}
}
}

View File

@@ -71,6 +71,18 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
DescriptionLink = "https://aka.ms/PowerToysOverview",
Link = "https://github.com/microsoft/PowerToys/releases/",
});
Modules.Insert((int)PowerToysModulesEnum.AlwaysOnTop, new OobePowerToysModule()
{
ModuleName = loader.GetString("Oobe_AlwaysOnTop"),
Tag = "AlwaysOnTop",
IsNew = true,
Icon = "\uEC32",
Image = "ms-appx:///Assets/Modules/AlwaysOnTop.png",
FluentIcon = "ms-appx:///Assets/FluentIcons/FluentIconsAlwaysOnTop.png",
PreviewImageSource = "ms-appx:///Assets/Modules/OOBE/AlwaysOnTop.png",
Description = loader.GetString("Oobe_AlwaysOnTop_Description"),
Link = "https://aka.ms/PowerToysOverview_AlwaysOnTop",
});
Modules.Insert((int)PowerToysModulesEnum.Awake, new OobePowerToysModule()
{
ModuleName = loader.GetString("Oobe_Awake"),
@@ -230,6 +242,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
switch (selectedItem.Tag)
{
case "Overview": NavigationFrame.Navigate(typeof(OobeOverview)); break;
case "AlwaysOnTop": NavigationFrame.Navigate(typeof(OobeAlwaysOnTop)); break;
case "Awake": NavigationFrame.Navigate(typeof(OobeAwake)); break;
case "ColorPicker": NavigationFrame.Navigate(typeof(OobeColorPicker)); break;
case "FancyZones": NavigationFrame.Navigate(typeof(OobeFancyZones)); break;

View File

@@ -143,6 +143,9 @@
<Compile Include="OOBE\Enums\PowerToysModulesEnum.cs" />
<Compile Include="OOBE\ViewModel\OobeShellViewModel.cs" />
<Compile Include="OOBE\ViewModel\OobePowerToysModule.cs" />
<Compile Include="OOBE\Views\OobeAlwaysOnTop.xaml.cs">
<DependentUpon>OobeAlwaysOnTop.xaml</DependentUpon>
</Compile>
<Compile Include="OOBE\Views\OobeAwake.xaml.cs">
<DependentUpon>OobeAwake.xaml</DependentUpon>
</Compile>
@@ -186,6 +189,9 @@
<Compile Include="Services\NavigationService.cs" />
<Compile Include="ViewModels\Commands\ButtonClickCommand.cs" />
<Compile Include="ViewModels\ShellViewModel.cs" />
<Compile Include="Views\AlwaysOnTopPage.xaml.cs">
<DependentUpon>AlwaysOnTopPage.xaml</DependentUpon>
</Compile>
<Compile Include="Views\AwakePage.xaml.cs">
<DependentUpon>AwakePage.xaml</DependentUpon>
</Compile>
@@ -233,6 +239,7 @@
</AppxManifest>
</ItemGroup>
<ItemGroup>
<Content Include="Assets\FluentIcons\FluentIconsAlwaysOnTop.png" />
<Content Include="Assets\FluentIcons\FluentIconsColorPicker.png" />
<Content Include="Assets\FluentIcons\FluentIconsAwake.png" />
<Content Include="Assets\FluentIcons\FluentIconsFancyZones.png" />
@@ -250,12 +257,14 @@
<Content Include="Assets\FluentIcons\FluentIconsShortcutGuide.png" />
<Content Include="Assets\FluentIcons\FluentIconsVideoConferenceMute.png" />
<Content Include="Assets\Logo.scale-200.png" />
<Content Include="Assets\Modules\AlwaysOnTop.png" />
<Content Include="Assets\Modules\ColorPicker.png" />
<Content Include="Assets\Modules\Awake.png" />
<Content Include="Assets\Modules\FancyZones.png" />
<Content Include="Assets\Modules\ImageResizer.png" />
<Content Include="Assets\Modules\KBM.png" />
<Content Include="Assets\Modules\MouseUtils.png" />
<Content Include="Assets\Modules\OOBE\AlwaysOnTop.png" />
<Content Include="Assets\Modules\OOBE\ColorPicker.gif" />
<Content Include="Assets\Modules\OOBE\Awake.png" />
<Content Include="Assets\Modules\OOBE\FancyZones.gif" />
@@ -368,6 +377,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="OOBE\Views\OobeAlwaysOnTop.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="OOBE\Views\OobeAwake.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
@@ -440,6 +453,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\AlwaysOnTopPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\AwakePage.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>

View File

@@ -1870,4 +1870,83 @@ From there, simply click on a Markdown file, PDF file or SVG icon in the File Ex
<value>On</value>
<comment>The state of a ToggleSwitch when it's on</comment>
</data>
<data name="AlwaysOnTop.ModuleDescription" xml:space="preserve">
<value>Always On Top is a quick and easy way to pin windows on top.</value>
<comment>"Always On Top" is the name of the utility</comment>
</data>
<data name="AlwaysOnTop.ModuleTitle" xml:space="preserve">
<value>Always On Top </value>
<comment>"Always On Top" is the name of the utility</comment>
</data>
<data name="AlwaysOnTop_Activation_GroupSettings.Header" xml:space="preserve">
<value>Activation</value>
</data>
<data name="AlwaysOnTop_EnableToggleControl_HeaderText.Header" xml:space="preserve">
<value>Enable Always On Top</value>
<comment>"Always On Top" is the name of the utility</comment>
</data>
<data name="AlwaysOnTop_ExcludedApps.Description" xml:space="preserve">
<value>Excludes an application from pinning on top</value>
</data>
<data name="AlwaysOnTop_ExcludedApps.Header" xml:space="preserve">
<value>Excluded apps</value>
</data>
<data name="AlwaysOnTop_ExcludedApps_TextBoxControl.PlaceholderText" xml:space="preserve">
<value>Example: outlook.exe</value>
</data>
<data name="AlwaysOnTop_FrameColor.Header" xml:space="preserve">
<value>Color</value>
</data>
<data name="AlwaysOnTop_FrameEnabled.Header" xml:space="preserve">
<value>Show a border around the pinned window</value>
</data>
<data name="AlwaysOnTop_FrameThickness.Header" xml:space="preserve">
<value>Thickness (px)</value>
<comment>px = pixels</comment>
</data>
<data name="AlwaysOnTop_Behavior_GroupSettings.Header" xml:space="preserve">
<value>Appearance &amp; behavior</value>
</data>
<data name="Shell_AlwaysOnTop.Content" xml:space="preserve">
<value>Always On Top</value>
<comment>"Always On Top" is the name of the utility</comment>
</data>
<data name="AlwaysOnTop_GameMode.Content" xml:space="preserve">
<value>Do not activate when Game Mode is on</value>
<comment>Game Mode is a Windows feature</comment>
</data>
<data name="AlwaysOnTop_SoundTitle.Header" xml:space="preserve">
<value>Sound</value>
</data>
<data name="AlwaysOnTop_Sound.Content" xml:space="preserve">
<value>Play a sound when pinning a window</value>
</data>
<data name="AlwaysOnTop_Behavior.Header" xml:space="preserve">
<value>Behavior</value>
</data>
<data name="LearnMore_AlwaysOnTop.Text" xml:space="preserve">
<value>Learn more about Always On Top</value>
<comment>"Always On Top" is the name of the utility</comment>
</data>
<data name="AlwaysOnTop_ActivationShortcut.Header" xml:space="preserve">
<value>Activation shortcut</value>
</data>
<data name="AlwaysOnTop_ActivationShortcut.Description" xml:space="preserve">
<value>Customize the shortcut to pin or unpin an app window</value>
<comment>"Always On Top" is the name of the utility</comment>
</data>
<data name="Oobe_AlwaysOnTop" xml:space="preserve">
<value>Always On Top</value>
<comment>"Always On Top" is the name of the utility</comment>
</data>
<data name="Oobe_AlwaysOnTop_Description" xml:space="preserve">
<value>Always On Top improves your multitasking workflow by pinning an application window so it's always in front - even when focus changes to another window after that.</value>
<comment>"Always On Top" is the name of the utility</comment>
</data>
<data name="Oobe_AlwaysOnTop_HowToUse.Text" xml:space="preserve">
<value>to pin or unpin the selected window so it's always on top of all other windows.</value>
</data>
<data name="Oobe_AlwaysOnTop_TipsAndTricks.Text" xml:space="preserve">
<value>You can tweak the visual outline of the pinned windows in PowerToys settings.</value>
</data>
</root>

View File

@@ -0,0 +1,112 @@
<Page
x:Class="Microsoft.PowerToys.Settings.UI.Views.AlwaysOnTopPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
mc:Ignorable="d"
AutomationProperties.LandmarkType="Main">
<controls:SettingsPageControl x:Uid="AlwaysOnTop" IsTabStop="False"
ModuleImageSource="ms-appx:///Assets/Modules/AlwaysOnTop.png">
<controls:SettingsPageControl.ModuleContent>
<StackPanel Orientation="Vertical">
<controls:Setting x:Uid="AlwaysOnTop_EnableToggleControl_HeaderText">
<controls:Setting.Icon>
<BitmapIcon UriSource="ms-appx:///Assets/FluentIcons/FluentIconsAlwaysOnTop.png" ShowAsMonochrome="False" />
</controls:Setting.Icon>
<controls:Setting.ActionContent>
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" x:Uid="ToggleSwitch"/>
</controls:Setting.ActionContent>
</controls:Setting>
<controls:SettingsGroup x:Uid="AlwaysOnTop_Activation_GroupSettings" IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabled}">
<controls:SettingExpander IsExpanded="True">
<controls:SettingExpander.Header>
<controls:Setting x:Uid="AlwaysOnTop_ActivationShortcut" Icon="&#xEDA7;">
<controls:Setting.ActionContent>
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.Hotkey, Mode=TwoWay}"/>
</controls:Setting.ActionContent>
</controls:Setting>
</controls:SettingExpander.Header>
<controls:SettingExpander.Content>
<StackPanel>
<CheckBox IsChecked="{x:Bind Mode=TwoWay, Path=ViewModel.DoNotActivateOnGameMode}" x:Uid="AlwaysOnTop_GameMode" Margin="{StaticResource ExpanderSettingMargin}"/>
</StackPanel>
</controls:SettingExpander.Content>
</controls:SettingExpander>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="AlwaysOnTop_Behavior_GroupSettings" IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabled}">
<controls:SettingExpander IsExpanded="True">
<controls:SettingExpander.Header>
<controls:Setting Icon="&#xE790;" Style="{StaticResource ExpanderHeaderSettingStyle}" x:Uid="AlwaysOnTop_FrameEnabled">
<controls:Setting.ActionContent>
<ToggleSwitch IsOn="{x:Bind Mode=TwoWay, Path=ViewModel.FrameEnabled}" x:Uid="ToggleSwitch"/>
</controls:Setting.ActionContent>
</controls:Setting>
</controls:SettingExpander.Header>
<controls:SettingExpander.Content>
<StackPanel>
<controls:Setting x:Uid="AlwaysOnTop_FrameColor" Style="{StaticResource ExpanderContentSettingStyle}" IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.FrameEnabled}">
<controls:Setting.ActionContent>
<controls:ColorPickerButton SelectedColor="{x:Bind Path=ViewModel.FrameColor, Mode=TwoWay}" />
</controls:Setting.ActionContent>
</controls:Setting>
<controls:Setting x:Uid="AlwaysOnTop_FrameThickness" Style="{StaticResource ExpanderContentSettingStyle}" IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.FrameEnabled}">
<controls:Setting.ActionContent>
<Slider Value="{x:Bind ViewModel.FrameThickness, Mode=TwoWay}"
Minimum="0"
MinWidth="{StaticResource SettingActionControlMinWidth}"
HorizontalAlignment="Right"
Maximum="30"
SmallChange="1"
LargeChange="5"/>
</controls:Setting.ActionContent>
</controls:Setting>
</StackPanel>
</controls:SettingExpander.Content>
</controls:SettingExpander>
<controls:SettingExpander IsExpanded="True">
<controls:SettingExpander.Header>
<controls:Setting Style="{StaticResource ExpanderHeaderSettingStyle}" x:Uid="AlwaysOnTop_SoundTitle" Icon="&#xE7F3;"/>
</controls:SettingExpander.Header>
<controls:SettingExpander.Content>
<StackPanel>
<CheckBox IsChecked="{x:Bind Mode=TwoWay, Path=ViewModel.SoundEnabled}" x:Uid="AlwaysOnTop_Sound" Margin="{StaticResource ExpanderSettingMargin}"/>
</StackPanel>
</controls:SettingExpander.Content>
</controls:SettingExpander>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="ExcludedApps" IsEnabled="{x:Bind Mode=OneWay, Path=ViewModel.IsEnabled}">
<controls:SettingExpander IsExpanded="True">
<controls:SettingExpander.Header>
<controls:Setting x:Uid="AlwaysOnTop_ExcludedApps" Icon="&#xECE4;" Style="{StaticResource ExpanderHeaderSettingStyle}"/>
</controls:SettingExpander.Header>
<controls:SettingExpander.Content>
<TextBox x:Uid="FancyZones_ExcludedApps_TextBoxControl"
Margin="{StaticResource ExpanderSettingMargin}"
Text="{x:Bind Mode=TwoWay, Path=ViewModel.ExcludedApps, UpdateSourceTrigger=PropertyChanged}"
ScrollViewer.VerticalScrollBarVisibility ="Visible"
ScrollViewer.VerticalScrollMode="Enabled"
ScrollViewer.IsVerticalRailEnabled="True"
TextWrapping="Wrap"
AcceptsReturn="True"
MinWidth="240"
MinHeight="160" />
</controls:SettingExpander.Content>
</controls:SettingExpander>
</controls:SettingsGroup>
</StackPanel>
</controls:SettingsPageControl.ModuleContent>
<controls:SettingsPageControl.PrimaryLinks>
<controls:PageLink x:Uid="LearnMore_AlwaysOnTop" Link="https://aka.ms/PowerToysOverview_AlwaysOnTop"/>
</controls:SettingsPageControl.PrimaryLinks>
</controls:SettingsPageControl>
</Page>

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.ViewModels;
using Windows.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
public sealed partial class AlwaysOnTopPage : Page
{
private AlwaysOnTopViewModel ViewModel { get; set; }
public AlwaysOnTopPage()
{
var settingsUtils = new SettingsUtils();
ViewModel = new AlwaysOnTopViewModel(settingsUtils, SettingsRepository<GeneralSettings>.GetInstance(settingsUtils), SettingsRepository<AlwaysOnTopSettings>.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
InitializeComponent();
}
}
}

View File

@@ -48,6 +48,14 @@
</muxc:NavigationViewItem.Icon>
</muxc:NavigationViewItem>
<muxc:NavigationViewItem x:Uid="Shell_AlwaysOnTop"
helpers:NavHelper.NavigateTo="views:AlwaysOnTopPage">
<muxc:NavigationViewItem.Icon>
<BitmapIcon UriSource="ms-appx:///Assets/FluentIcons/FluentIconsAlwaysOnTop.png"
ShowAsMonochrome="False" />
</muxc:NavigationViewItem.Icon>
</muxc:NavigationViewItem>
<muxc:NavigationViewItem x:Uid="Shell_Awake"
helpers:NavHelper.NavigateTo="views:AwakePage">
<muxc:NavigationViewItem.Icon>

View File

@@ -16,5 +16,6 @@ std::vector<std::wstring> processes =
L"PowerToys.PowerRename.exe",
L"PowerToys.ImageResizer.exe",
L"PowerToys.Update.exe",
L"PowerToys.ActionRunner.exe"
L"PowerToys.ActionRunner.exe",
L"PowerToys.AlwaysOnTop.exe"
};