[AlwaysOnTop] Proof of concept (#14360)
Co-authored-by: Niels Laute <niels.laute@live.nl>
9
.github/actions/spell-check/expect.txt
vendored
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
doc/images/icons/Always On Top.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
doc/images/overview/AlwaysOnTop_large.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
doc/images/overview/AlwaysOnTop_small.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
doc/images/overview/Original/AlwaysOnTop.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
415
src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.h
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
203
src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.vcxproj
Normal 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>
|
||||
@@ -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>
|
||||
172
src/modules/alwaysontop/AlwaysOnTop/FrameDrawer.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/modules/alwaysontop/AlwaysOnTop/FrameDrawer.h
Normal 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;
|
||||
};
|
||||
6
src/modules/alwaysontop/AlwaysOnTop/ModuleConstants.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
namespace NonLocalizable
|
||||
{
|
||||
const inline wchar_t ModuleKey[] = L"AlwaysOnTop";
|
||||
}
|
||||
188
src/modules/alwaysontop/AlwaysOnTop/Settings.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/modules/alwaysontop/AlwaysOnTop/Settings.h
Normal 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;
|
||||
};
|
||||
12
src/modules/alwaysontop/AlwaysOnTop/SettingsConstants.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
enum class SettingId
|
||||
{
|
||||
Hotkey = 0,
|
||||
SoundEnabled,
|
||||
FrameEnabled,
|
||||
FrameThickness,
|
||||
FrameColor,
|
||||
BlockInGameMode,
|
||||
ExcludeApps
|
||||
};
|
||||
31
src/modules/alwaysontop/AlwaysOnTop/SettingsObserver.h
Normal 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;
|
||||
};
|
||||
44
src/modules/alwaysontop/AlwaysOnTop/Sound.h
Normal 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;
|
||||
};
|
||||
63
src/modules/alwaysontop/AlwaysOnTop/VirtualDesktopUtils.cpp
Normal 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;
|
||||
}
|
||||
17
src/modules/alwaysontop/AlwaysOnTop/VirtualDesktopUtils.h
Normal 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;
|
||||
};
|
||||
14
src/modules/alwaysontop/AlwaysOnTop/WinHookEventIDs.cpp
Normal 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}");
|
||||
});
|
||||
}
|
||||
5
src/modules/alwaysontop/AlwaysOnTop/WinHookEventIDs.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
extern UINT WM_PRIV_SETTINGS_CHANGED; // Scheduled when the a watched settings file is updated
|
||||
|
||||
void InitializeWinhookEventIds();
|
||||
223
src/modules/alwaysontop/AlwaysOnTop/WindowBorder.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
45
src/modules/alwaysontop/AlwaysOnTop/WindowBorder.h
Normal 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;
|
||||
};
|
||||
62
src/modules/alwaysontop/AlwaysOnTop/main.cpp
Normal 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;
|
||||
}
|
||||
5
src/modules/alwaysontop/AlwaysOnTop/packages.config
Normal 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>
|
||||
1
src/modules/alwaysontop/AlwaysOnTop/pch.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "pch.h"
|
||||
7
src/modules/alwaysontop/AlwaysOnTop/pch.h
Normal 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>
|
||||
35
src/modules/alwaysontop/AlwaysOnTop/trace.cpp
Normal 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));
|
||||
}
|
||||
14
src/modules/alwaysontop/AlwaysOnTop/trace.h
Normal 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;
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
159
src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp
Normal 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();
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
#include "pch.h"
|
||||
17
src/modules/alwaysontop/AlwaysOnTopModuleInterface/pch.h
Normal 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;
|
||||
}
|
||||
BIN
src/modules/alwaysontop/AlwaysOnTopModuleInterface/targetver.h
Normal 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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
57
src/settings-ui/Settings.UI.Library/AlwaysOnTopProperties.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/settings-ui/Settings.UI.Library/AlwaysOnTopSettings.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 10 KiB |
BIN
src/settings-ui/Settings.UI/Assets/Modules/AlwaysOnTop.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
src/settings-ui/Settings.UI/Assets/Modules/OOBE/AlwaysOnTop.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
@@ -7,6 +7,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
|
||||
public enum PowerToysModulesEnum
|
||||
{
|
||||
Overview = 0,
|
||||
AlwaysOnTop,
|
||||
Awake,
|
||||
ColorPicker,
|
||||
FancyZones,
|
||||
|
||||
41
src/settings-ui/Settings.UI/OOBE/Views/OobeAlwaysOnTop.xaml
Normal 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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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>
|
||||
112
src/settings-ui/Settings.UI/Views/AlwaysOnTopPage.xaml
Normal 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="">
|
||||
<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="" 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=""/>
|
||||
</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="" 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>
|
||||
23
src/settings-ui/Settings.UI/Views/AlwaysOnTopPage.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
|
||||