mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-06 19:26:39 +02:00
[FancyZones]Modern apps snapping fix, refactor and tests (#29499)
* popup windows check
* use minimize maximize buttons check
* update test utils
* added tests
* define types for easier testing
* changed checks order
* remove option check
* upd test
* remove FZ popup option
* max min buttons -> caption
* calculator test
* updated excluded tests
* add asserts to child window test
* update window creation
* splash screen refactor
* remove hotfix part
* replace style checking functions
* remove no longer necessary check
* tool window check fix
* fix mouse snapping check
* added check and test for non-root window
* spelling
* Revert "remove FZ popup option"
This reverts commit 26732ad683.
* skip child window tests in CI
* remove the option
* remove the constant
* updated tests
This commit is contained in:
@@ -394,15 +394,6 @@ void FancyZones::WindowCreated(HWND window) noexcept
|
||||
return;
|
||||
}
|
||||
|
||||
// Hotfix
|
||||
// Avoid automatically moving popup windows, as they can be just popup menus.
|
||||
bool isPopup = FancyZonesWindowUtils::IsPopupWindow(window);
|
||||
bool hasThickFrame = FancyZonesWindowUtils::HasThickFrame(window);
|
||||
if (isPopup && !hasThickFrame)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid already stamped (zoned) windows
|
||||
const bool isZoned = !FancyZonesWindowProperties::RetrieveZoneIndexProperty(window).empty();
|
||||
if (isZoned)
|
||||
@@ -1005,41 +996,42 @@ void FancyZones::RefreshLayouts() noexcept
|
||||
|
||||
bool FancyZones::ShouldProcessSnapHotkey(DWORD vkCode) noexcept
|
||||
{
|
||||
auto window = GetForegroundWindow();
|
||||
if (!FancyZonesSettings::settings().overrideSnapHotkeys)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto window = GetForegroundWindow();
|
||||
if (!FancyZonesWindowProcessing::IsProcessable(window))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FancyZonesSettings::settings().overrideSnapHotkeys)
|
||||
HMONITOR monitor = WorkAreaKeyFromWindow(window);
|
||||
|
||||
auto workArea = m_workAreaConfiguration.GetWorkArea(monitor);
|
||||
if (!workArea)
|
||||
{
|
||||
HMONITOR monitor = WorkAreaKeyFromWindow(window);
|
||||
Logger::error(L"No work area for processing snap hotkey");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto workArea = m_workAreaConfiguration.GetWorkArea(monitor);
|
||||
if (!workArea)
|
||||
const auto& layout = workArea->GetLayout();
|
||||
if (!layout)
|
||||
{
|
||||
Logger::error(L"No layout for processing snap hotkey");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (layout->Zones().size() > 0)
|
||||
{
|
||||
if (vkCode == VK_UP || vkCode == VK_DOWN)
|
||||
{
|
||||
Logger::error(L"No work area for processing snap hotkey");
|
||||
return false;
|
||||
return FancyZonesSettings::settings().moveWindowsBasedOnPosition;
|
||||
}
|
||||
|
||||
const auto& layout = workArea->GetLayout();
|
||||
if (!layout)
|
||||
else
|
||||
{
|
||||
Logger::error(L"No layout for processing snap hotkey");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (layout->Zones().size() > 0)
|
||||
{
|
||||
if (vkCode == VK_UP || vkCode == VK_DOWN)
|
||||
{
|
||||
return FancyZonesSettings::settings().moveWindowsBasedOnPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,52 +5,68 @@
|
||||
#include <FancyZonesLib/VirtualDesktop.h>
|
||||
#include <FancyZonesLib/WindowUtils.h>
|
||||
|
||||
bool FancyZonesWindowProcessing::IsProcessable(HWND window) noexcept
|
||||
FancyZonesWindowProcessing::ProcessabilityType FancyZonesWindowProcessing::DefineWindowType(HWND window) noexcept
|
||||
{
|
||||
const bool isSplashScreen = FancyZonesWindowUtils::IsSplashScreen(window);
|
||||
if (isSplashScreen)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool windowMinimized = IsIconic(window);
|
||||
if (windowMinimized)
|
||||
{
|
||||
return false;
|
||||
return ProcessabilityType::Minimized;
|
||||
}
|
||||
|
||||
const bool standard = FancyZonesWindowUtils::IsStandardWindow(window);
|
||||
if (!standard)
|
||||
auto style = GetWindowLong(window, GWL_STYLE);
|
||||
auto exStyle = GetWindowLong(window, GWL_EXSTYLE);
|
||||
|
||||
if (!FancyZonesWindowUtils::HasStyle(style, WS_VISIBLE))
|
||||
{
|
||||
return false;
|
||||
return ProcessabilityType::NotVisible;
|
||||
}
|
||||
|
||||
// popup could be the window we don't want to snap: start menu, notification popup, tray window, etc.
|
||||
// also, popup could be the windows we want to snap disregarding the "allowSnapPopupWindows" setting, e.g. Telegram
|
||||
bool isPopup = FancyZonesWindowUtils::IsPopupWindow(window) && !FancyZonesWindowUtils::HasThickFrameAndMinimizeMaximizeButtons(window);
|
||||
if (isPopup && !FancyZonesSettings::settings().allowSnapPopupWindows)
|
||||
if (FancyZonesWindowUtils::HasStyle(exStyle, WS_EX_TOOLWINDOW))
|
||||
{
|
||||
return false;
|
||||
return ProcessabilityType::ToolWindow;
|
||||
}
|
||||
|
||||
if (!FancyZonesWindowUtils::IsRoot(window))
|
||||
{
|
||||
// child windows such as buttons, combo boxes, etc.
|
||||
return ProcessabilityType::NonRootWindow;
|
||||
}
|
||||
|
||||
bool isPopup = FancyZonesWindowUtils::HasStyle(style, WS_POPUP);
|
||||
bool hasThickFrame = FancyZonesWindowUtils::HasStyle(style, WS_THICKFRAME);
|
||||
bool hasCaption = FancyZonesWindowUtils::HasStyle(style, WS_CAPTION);
|
||||
if (isPopup && !(hasThickFrame && hasCaption))
|
||||
{
|
||||
// popup windows we want to snap: e.g. Calculator, Telegram
|
||||
// popup windows we don't want to snap: start menu, notification popup, tray window, etc.
|
||||
// WS_CAPTION is used for filtering out menus,
|
||||
// e.g., in Edge "Running as admin" menu when creating a new PowerToys issue.
|
||||
return ProcessabilityType::NonProcessablePopupWindow;
|
||||
}
|
||||
|
||||
// allow child windows
|
||||
auto hasOwner = FancyZonesWindowUtils::HasVisibleOwner(window);
|
||||
if (hasOwner && !FancyZonesSettings::settings().allowSnapChildWindows)
|
||||
{
|
||||
return false;
|
||||
return ProcessabilityType::ChildWindow;
|
||||
}
|
||||
|
||||
if (FancyZonesWindowUtils::IsExcluded(window))
|
||||
{
|
||||
return false;
|
||||
return ProcessabilityType::Excluded;
|
||||
}
|
||||
|
||||
// Switch between virtual desktops results with posting same windows messages that also indicate
|
||||
// creation of new window. We need to check if window being processed is on currently active desktop.
|
||||
if (!VirtualDesktop::instance().IsWindowOnCurrentDesktop(window))
|
||||
{
|
||||
return false;
|
||||
return ProcessabilityType::NotCurrentVirtualDesktop;
|
||||
}
|
||||
|
||||
return true;
|
||||
return ProcessabilityType::Processable;
|
||||
}
|
||||
|
||||
bool FancyZonesWindowProcessing::IsProcessable(HWND window) noexcept
|
||||
{
|
||||
return DefineWindowType(window) == ProcessabilityType::Processable;
|
||||
}
|
||||
|
||||
@@ -2,5 +2,20 @@
|
||||
|
||||
namespace FancyZonesWindowProcessing
|
||||
{
|
||||
enum class ProcessabilityType
|
||||
{
|
||||
Processable = 0,
|
||||
SplashScreen,
|
||||
Minimized,
|
||||
ToolWindow,
|
||||
NotVisible,
|
||||
NonRootWindow,
|
||||
NonProcessablePopupWindow,
|
||||
ChildWindow,
|
||||
Excluded,
|
||||
NotCurrentVirtualDesktop
|
||||
};
|
||||
|
||||
ProcessabilityType DefineWindowType(HWND window) noexcept;
|
||||
bool IsProcessable(HWND window) noexcept;
|
||||
}
|
||||
@@ -33,7 +33,6 @@ namespace NonLocalizable
|
||||
const wchar_t ShowOnAllMonitorsID[] = L"fancyzones_show_on_all_monitors";
|
||||
const wchar_t SpanZonesAcrossMonitorsID[] = L"fancyzones_span_zones_across_monitors";
|
||||
const wchar_t MakeDraggedWindowTransparentID[] = L"fancyzones_makeDraggedWindowTransparent";
|
||||
const wchar_t AllowPopupWindowSnapID[] = L"fancyzones_allowPopupWindowSnap";
|
||||
const wchar_t AllowChildWindowSnapID[] = L"fancyzones_allowChildWindowSnap";
|
||||
const wchar_t DisableRoundCornersOnSnapping[] = L"fancyzones_disableRoundCornersOnSnap";
|
||||
|
||||
@@ -127,7 +126,6 @@ void FancyZonesSettings::LoadSettings()
|
||||
SetBoolFlag(values, NonLocalizable::WindowSwitchingToggleID, SettingId::WindowSwitching, m_settings.windowSwitching);
|
||||
SetBoolFlag(values, NonLocalizable::SystemThemeID, SettingId::SystemTheme, m_settings.systemTheme);
|
||||
SetBoolFlag(values, NonLocalizable::ShowZoneNumberID, SettingId::ShowZoneNumber, m_settings.showZoneNumber);
|
||||
SetBoolFlag(values, NonLocalizable::AllowPopupWindowSnapID, SettingId::AllowSnapPopupWindows, m_settings.allowSnapPopupWindows);
|
||||
SetBoolFlag(values, NonLocalizable::AllowChildWindowSnapID, SettingId::AllowSnapChildWindows, m_settings.allowSnapChildWindows);
|
||||
SetBoolFlag(values, NonLocalizable::DisableRoundCornersOnSnapping, SettingId::DisableRoundCornersOnSnapping, m_settings.disableRoundCorners);
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ struct Settings
|
||||
bool makeDraggedWindowTransparent = true;
|
||||
bool systemTheme = true;
|
||||
bool showZoneNumber = true;
|
||||
bool allowSnapPopupWindows = false;
|
||||
bool allowSnapChildWindows = false;
|
||||
bool disableRoundCorners = false;
|
||||
std::wstring zoneColor = L"#AACDFF";
|
||||
|
||||
@@ -32,7 +32,6 @@ enum class SettingId
|
||||
NextTabHotkey,
|
||||
PrevTabHotkey,
|
||||
ExcludedApps,
|
||||
AllowSnapPopupWindows,
|
||||
AllowSnapChildWindows,
|
||||
DisableRoundCornersOnSnapping,
|
||||
};
|
||||
@@ -20,8 +20,6 @@ WindowMouseSnap::WindowMouseSnap(HWND window, const std::unordered_map<HMONITOR,
|
||||
m_snappingMode(false)
|
||||
{
|
||||
m_windowProperties.hasNoVisibleOwner = !FancyZonesWindowUtils::HasVisibleOwner(m_window);
|
||||
m_windowProperties.isStandardWindow = FancyZonesWindowUtils::IsStandardWindow(m_window) &&
|
||||
(!FancyZonesWindowUtils::IsPopupWindow(m_window) || FancyZonesSettings::settings().allowSnapPopupWindows);
|
||||
}
|
||||
|
||||
WindowMouseSnap::~WindowMouseSnap()
|
||||
@@ -31,8 +29,7 @@ WindowMouseSnap::~WindowMouseSnap()
|
||||
|
||||
std::unique_ptr<WindowMouseSnap> WindowMouseSnap::Create(HWND window, const std::unordered_map<HMONITOR, std::unique_ptr<WorkArea>>& activeWorkAreas)
|
||||
{
|
||||
if (!FancyZonesWindowProcessing::IsProcessable(window) ||
|
||||
FancyZonesWindowUtils::IsCursorTypeIndicatingSizeEvent())
|
||||
if (FancyZonesWindowUtils::IsCursorTypeIndicatingSizeEvent() || !FancyZonesWindowProcessing::IsProcessable(window))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
@@ -111,13 +108,8 @@ void WindowMouseSnap::MoveSizeUpdate(HMONITOR monitor, POINT const& ptScreen, bo
|
||||
void WindowMouseSnap::MoveSizeEnd()
|
||||
{
|
||||
if (m_snappingMode)
|
||||
{
|
||||
const bool hasNoVisibleOwner = !FancyZonesWindowUtils::HasVisibleOwner(m_window);
|
||||
const bool isStandardWindow = FancyZonesWindowUtils::IsStandardWindow(m_window);
|
||||
|
||||
if ((isStandardWindow == false && hasNoVisibleOwner == true &&
|
||||
m_windowProperties.isStandardWindow == true && m_windowProperties.hasNoVisibleOwner == true) ||
|
||||
FancyZonesWindowUtils::IsWindowMaximized(m_window))
|
||||
{
|
||||
if (FancyZonesWindowUtils::IsWindowMaximized(m_window))
|
||||
{
|
||||
// Abort the zoning, this is a Chromium based tab that is merged back with an existing window
|
||||
// or if the window is maximized by Windows when the cursor hits the screen top border
|
||||
|
||||
@@ -24,8 +24,6 @@ private:
|
||||
|
||||
struct WindowProperties
|
||||
{
|
||||
// True if from the styles the window looks like a standard window
|
||||
bool isStandardWindow = false;
|
||||
// True if the window is a top-level window that does not have a visible owner
|
||||
bool hasNoVisibleOwner = false;
|
||||
// Properties to restore after dragging
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
namespace NonLocalizable
|
||||
{
|
||||
const wchar_t PowerToysAppFZEditor[] = L"POWERTOYS.FANCYZONESEDITOR.EXE";
|
||||
const wchar_t SplashClassName[] = L"MsoSplash";
|
||||
const char SplashClassName[] = "MsoSplash";
|
||||
const wchar_t CoreWindow[] = L"Windows.UI.Core.CoreWindow";
|
||||
const wchar_t SearchUI[] = L"SearchUI.exe";
|
||||
const wchar_t SystemAppsFolder[] = L"SYSTEMAPPS";
|
||||
@@ -122,17 +122,6 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
bool FancyZonesWindowUtils::IsSplashScreen(HWND window)
|
||||
{
|
||||
wchar_t className[MAX_PATH];
|
||||
if (GetClassName(window, className, MAX_PATH) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return wcscmp(NonLocalizable::SplashClassName, className) == 0;
|
||||
}
|
||||
|
||||
bool FancyZonesWindowUtils::IsWindowMaximized(HWND window) noexcept
|
||||
{
|
||||
WINDOWPLACEMENT placement{};
|
||||
@@ -164,44 +153,9 @@ bool FancyZonesWindowUtils::HasVisibleOwner(HWND window) noexcept
|
||||
return rect.top != rect.bottom && rect.left != rect.right;
|
||||
}
|
||||
|
||||
bool FancyZonesWindowUtils::IsStandardWindow(HWND window)
|
||||
bool FancyZonesWindowUtils::IsRoot(HWND window) noexcept
|
||||
{
|
||||
// True if from the styles the window looks like a standard window
|
||||
|
||||
if (GetAncestor(window, GA_ROOT) != window)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto style = GetWindowLong(window, GWL_STYLE);
|
||||
auto exStyle = GetWindowLong(window, GWL_EXSTYLE);
|
||||
|
||||
bool isToolWindow = (exStyle & WS_EX_TOOLWINDOW) == WS_EX_TOOLWINDOW;
|
||||
bool isVisible = (style & WS_VISIBLE) == WS_VISIBLE;
|
||||
if (isToolWindow || !isVisible)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FancyZonesWindowUtils::IsPopupWindow(HWND window) noexcept
|
||||
{
|
||||
auto style = GetWindowLong(window, GWL_STYLE);
|
||||
return ((style & WS_POPUP) == WS_POPUP);
|
||||
}
|
||||
|
||||
bool FancyZonesWindowUtils::HasThickFrame(HWND window) noexcept
|
||||
{
|
||||
auto style = GetWindowLong(window, GWL_STYLE);
|
||||
return ((style & WS_THICKFRAME) == WS_THICKFRAME);
|
||||
}
|
||||
|
||||
bool FancyZonesWindowUtils::HasThickFrameAndMinimizeMaximizeButtons(HWND window) noexcept
|
||||
{
|
||||
auto style = GetWindowLong(window, GWL_STYLE);
|
||||
return ((style & WS_THICKFRAME) == WS_THICKFRAME && (style & WS_MINIMIZEBOX) == WS_MINIMIZEBOX && (style & WS_MAXIMIZEBOX) == WS_MAXIMIZEBOX);
|
||||
return GetAncestor(window, GA_ROOT) == window;
|
||||
}
|
||||
|
||||
bool FancyZonesWindowUtils::IsProcessOfWindowElevated(HWND window)
|
||||
@@ -261,9 +215,14 @@ bool FancyZonesWindowUtils::IsExcludedByDefault(const HWND& hwnd, std::wstring&
|
||||
return true;
|
||||
}
|
||||
|
||||
std::array<char, 256> class_name;
|
||||
GetClassNameA(hwnd, class_name.data(), static_cast<int>(class_name.size()));
|
||||
if (is_system_window(hwnd, class_name.data()))
|
||||
std::array<char, 256> className;
|
||||
GetClassNameA(hwnd, className.data(), static_cast<int>(className.size()));
|
||||
if (is_system_window(hwnd, className.data()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strcmp(NonLocalizable::SplashClassName, className.data()) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -15,13 +15,15 @@
|
||||
|
||||
namespace FancyZonesWindowUtils
|
||||
{
|
||||
bool IsSplashScreen(HWND window);
|
||||
bool IsWindowMaximized(HWND window) noexcept;
|
||||
bool HasVisibleOwner(HWND window) noexcept;
|
||||
bool IsStandardWindow(HWND window);
|
||||
bool IsPopupWindow(HWND window) noexcept;
|
||||
bool HasThickFrame(HWND window) noexcept;
|
||||
bool HasThickFrameAndMinimizeMaximizeButtons(HWND window) noexcept;
|
||||
bool IsRoot(HWND window) noexcept;
|
||||
|
||||
constexpr bool HasStyle(LONG style, LONG styleToCheck) noexcept
|
||||
{
|
||||
return ((style & styleToCheck) == styleToCheck);
|
||||
}
|
||||
|
||||
bool IsProcessOfWindowElevated(HWND window); // If HWND is already dead, we assume it wasn't elevated
|
||||
|
||||
bool IsExcluded(HWND window);
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
#define SpanZonesAcrossMonitorsKey "SpanZonesAcrossMonitors"
|
||||
#define MakeDraggedWindowTransparentKey "MakeDraggedWindowTransparent"
|
||||
#define AllowSnapChildWindows "AllowSnapChildWindows"
|
||||
#define AllowSnapPopupWindows "AllowSnapPopupWindows"
|
||||
#define DisableRoundCornersOnSnapping "DisableRoundCornersOnSnapping"
|
||||
#define ZoneColorKey "ZoneColor"
|
||||
#define ZoneBorderColorKey "ZoneBorderColor"
|
||||
@@ -323,7 +322,6 @@ void Trace::SettingsTelemetry(const Settings& settings) noexcept
|
||||
TraceLoggingBoolean(settings.spanZonesAcrossMonitors, SpanZonesAcrossMonitorsKey),
|
||||
TraceLoggingBoolean(settings.makeDraggedWindowTransparent, MakeDraggedWindowTransparentKey),
|
||||
TraceLoggingBoolean(settings.allowSnapChildWindows, AllowSnapChildWindows),
|
||||
TraceLoggingBoolean(settings.allowSnapPopupWindows, AllowSnapPopupWindows),
|
||||
TraceLoggingBoolean(settings.disableRoundCorners, DisableRoundCornersOnSnapping),
|
||||
TraceLoggingWideString(settings.zoneColor.c_str(), ZoneColorKey),
|
||||
TraceLoggingWideString(settings.zoneBorderColor.c_str(), ZoneBorderColorKey),
|
||||
|
||||
Reference in New Issue
Block a user