mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-09-01 19:51:34 +02:00
Add FancyZones monitor window rotation (#48772)
# Summary Adds an opt-in FancyZones feature to rotate processable windows across connected monitors. The feature lets users hold the configured monitor rotation hotkey, preview the current monitor content order, and rotate windows left or right across monitor work areas using the arrow keys. The overlay keeps content numbers visually consistent while windows move between monitors. This change was prototyped and implemented with assistance from Codex. # Demo  # Details - Adds FancyZones settings for monitor rotation and its activation hotkey. - Adds Settings UI controls under FancyZones > Windows. - Extends the FancyZones keyboard hook to forward keyup events. - Adds window snapshot, monitor mapping, and work-area-relative rotation logic. - Adds a dark visual overlay with monitor-content numbering and directional transition hints. - Updates FancyZones settings parsing coverage. # Validation - Built `FancyZonesLib.vcxproj` successfully. - Built `FancyZones.vcxproj` successfully. - Built Settings UI projects successfully. - Built Runner successfully. - Ran focused FancyZones settings parse tests successfully. - Manual validation performed on a multi-monitor setup. # Notes The feature is disabled by default and must be enabled from FancyZones settings. --------- Co-authored-by: Muyuan Li (from Dev Box) <muyuanli@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2b3acca3-e49b-4936-8fb9-6f669bd449db Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7b3fb20d-6e9d-4fef-a5cd-f8921d28c220
This commit is contained in:
committed by
Boliang Zhang (from Dev Box)
parent
454beb9ac9
commit
d85fc2c7cc
BIN
doc/images/fancyzones/monitor-window-rotation.gif
Executable file
BIN
doc/images/fancyzones/monitor-window-rotation.gif
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 199 KiB |
@@ -188,5 +188,11 @@ void FancyZonesApp::HandleWinHookEvent(WinHookEvent* data) noexcept
|
||||
|
||||
intptr_t FancyZonesApp::HandleKeyboardHookEvent(LowlevelKeyboardEvent* data) noexcept
|
||||
{
|
||||
return m_app.as<IFancyZonesCallback>()->OnKeyDown(data->lParam);
|
||||
auto fzCallback = m_app.as<IFancyZonesCallback>();
|
||||
if (data->wParam == WM_KEYUP || data->wParam == WM_SYSKEYUP)
|
||||
{
|
||||
return fzCallback->OnKeyUp(data->lParam);
|
||||
}
|
||||
|
||||
return fzCallback->OnKeyDown(data->lParam);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ private:
|
||||
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
LowlevelKeyboardEvent event;
|
||||
if (nCode == HC_ACTION && wParam == WM_KEYDOWN)
|
||||
if (nCode == HC_ACTION && (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN || wParam == WM_KEYUP || wParam == WM_SYSKEYUP))
|
||||
{
|
||||
event.lParam = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
|
||||
event.wParam = wParam;
|
||||
|
||||
@@ -66,11 +66,158 @@ namespace NonLocalizable
|
||||
const wchar_t FZEditorExecutablePath[] = L"PowerToys.FancyZonesEditor.exe";
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr UINT_PTR MonitorRotationCommitTimerId = 0x4D525443;
|
||||
constexpr UINT MonitorRotationCommitDelayMillis = 760;
|
||||
|
||||
struct WindowRotationSnapshot
|
||||
{
|
||||
HWND window{};
|
||||
HMONITOR sourceMonitor{};
|
||||
RECT sourceRect{};
|
||||
};
|
||||
|
||||
std::optional<size_t> FindMonitorIndex(HMONITOR monitor, const std::vector<std::pair<HMONITOR, RECT>>& monitors) noexcept
|
||||
{
|
||||
const auto iter = std::find_if(monitors.begin(), monitors.end(), [monitor](const auto& entry) {
|
||||
return entry.first == monitor;
|
||||
});
|
||||
|
||||
if (iter == monitors.end())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return static_cast<size_t>(std::distance(monitors.begin(), iter));
|
||||
}
|
||||
|
||||
std::vector<WindowRotationSnapshot> CollectWindowRotationSnapshots()
|
||||
{
|
||||
std::vector<WindowRotationSnapshot> windows;
|
||||
EnumWindows(
|
||||
[](HWND window, LPARAM param) -> BOOL {
|
||||
if (!FancyZonesWindowProcessing::IsProcessableManually(window))
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
RECT rect{};
|
||||
if (!GetWindowRect(window, &rect) || rect.left == rect.right || rect.top == rect.bottom)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
auto sourceMonitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL);
|
||||
if (!sourceMonitor)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
auto& snapshots = *reinterpret_cast<std::vector<WindowRotationSnapshot>*>(param);
|
||||
snapshots.push_back({ .window = window, .sourceMonitor = sourceMonitor, .sourceRect = rect });
|
||||
return TRUE;
|
||||
},
|
||||
reinterpret_cast<LPARAM>(&windows));
|
||||
|
||||
return windows;
|
||||
}
|
||||
|
||||
constexpr bool IsRotationPreviewReleaseKey(DWORD vkCode) noexcept
|
||||
{
|
||||
return vkCode == VK_LWIN || vkCode == VK_RWIN || vkCode == VK_CONTROL || vkCode == VK_LCONTROL || vkCode == VK_RCONTROL || vkCode == VK_MENU || vkCode == VK_LMENU || vkCode == VK_RMENU || vkCode == VK_SHIFT || vkCode == VK_LSHIFT || vkCode == VK_RSHIFT;
|
||||
}
|
||||
|
||||
constexpr bool IsWinKey(DWORD vkCode) noexcept
|
||||
{
|
||||
return vkCode == VK_LWIN || vkCode == VK_RWIN;
|
||||
}
|
||||
|
||||
constexpr bool IsCtrlKey(DWORD vkCode) noexcept
|
||||
{
|
||||
return vkCode == VK_CONTROL || vkCode == VK_LCONTROL || vkCode == VK_RCONTROL;
|
||||
}
|
||||
|
||||
constexpr bool IsAltKey(DWORD vkCode) noexcept
|
||||
{
|
||||
return vkCode == VK_MENU || vkCode == VK_LMENU || vkCode == VK_RMENU;
|
||||
}
|
||||
|
||||
constexpr bool IsShiftKey(DWORD vkCode) noexcept
|
||||
{
|
||||
return vkCode == VK_SHIFT || vkCode == VK_LSHIFT || vkCode == VK_RSHIFT;
|
||||
}
|
||||
|
||||
size_t GetMonitorDisplayNumber(HMONITOR monitor, size_t fallback) noexcept
|
||||
{
|
||||
MONITORINFOEX monitorInfo{};
|
||||
monitorInfo.cbSize = sizeof(MONITORINFOEX);
|
||||
if (!GetMonitorInfoW(monitor, &monitorInfo))
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
std::wstring deviceName = monitorInfo.szDevice;
|
||||
const auto digitStart = deviceName.find_last_not_of(L"0123456789");
|
||||
if (digitStart == std::wstring::npos || digitStart + 1 >= deviceName.size())
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return static_cast<size_t>(std::stoul(deviceName.substr(digitStart + 1)));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace MonitorRotation
|
||||
{
|
||||
LONG ScaleCoordinate(LONG value, LONG sourceStart, LONG sourceSize, LONG targetStart, LONG targetSize) noexcept
|
||||
{
|
||||
if (sourceSize == 0)
|
||||
{
|
||||
return targetStart;
|
||||
}
|
||||
|
||||
return targetStart + MulDiv(value - sourceStart, targetSize, sourceSize);
|
||||
}
|
||||
|
||||
RECT MapRectBetweenMonitorWorkAreas(const RECT& windowRect, const RECT& sourceWorkArea, const RECT& targetWorkArea) noexcept
|
||||
{
|
||||
const LONG sourceWidth = sourceWorkArea.right - sourceWorkArea.left;
|
||||
const LONG sourceHeight = sourceWorkArea.bottom - sourceWorkArea.top;
|
||||
const LONG targetWidth = targetWorkArea.right - targetWorkArea.left;
|
||||
const LONG targetHeight = targetWorkArea.bottom - targetWorkArea.top;
|
||||
|
||||
return RECT{
|
||||
.left = ScaleCoordinate(windowRect.left, sourceWorkArea.left, sourceWidth, targetWorkArea.left, targetWidth),
|
||||
.top = ScaleCoordinate(windowRect.top, sourceWorkArea.top, sourceHeight, targetWorkArea.top, targetHeight),
|
||||
.right = ScaleCoordinate(windowRect.right, sourceWorkArea.left, sourceWidth, targetWorkArea.left, targetWidth),
|
||||
.bottom = ScaleCoordinate(windowRect.bottom, sourceWorkArea.top, sourceHeight, targetWorkArea.top, targetHeight),
|
||||
};
|
||||
}
|
||||
|
||||
size_t GetRotatedMonitorIndex(size_t sourceIndex, size_t monitorCount, bool reverse) noexcept
|
||||
{
|
||||
if (reverse)
|
||||
{
|
||||
return sourceIndex == 0 ? monitorCount - 1 : sourceIndex - 1;
|
||||
}
|
||||
|
||||
return (sourceIndex + 1) % monitorCount;
|
||||
}
|
||||
}
|
||||
|
||||
struct FancyZones : public winrt::implements<FancyZones, IFancyZones, IFancyZonesCallback>, public SettingsObserver
|
||||
{
|
||||
public:
|
||||
FancyZones(HINSTANCE hinstance, std::function<void()> disableModuleCallbackFunction) noexcept :
|
||||
SettingsObserver({ SettingId::EditorHotkey, SettingId::WindowSwitching, SettingId::PrevTabHotkey, SettingId::NextTabHotkey, SettingId::SpanZonesAcrossMonitors }),
|
||||
SettingsObserver({ SettingId::EditorHotkey, SettingId::WindowSwitching, SettingId::PrevTabHotkey, SettingId::NextTabHotkey, SettingId::SpanZonesAcrossMonitors, SettingId::MonitorRotation, SettingId::MonitorRotationHotkey }),
|
||||
m_hinstance(hinstance),
|
||||
m_draggingState([this]() {
|
||||
PostMessageW(m_window, WM_PRIV_LOCATIONCHANGE, NULL, NULL);
|
||||
@@ -143,6 +290,8 @@ public:
|
||||
VirtualDesktopChanged() noexcept;
|
||||
IFACEMETHODIMP_(bool)
|
||||
OnKeyDown(PKBDLLHOOKSTRUCT info) noexcept;
|
||||
IFACEMETHODIMP_(bool)
|
||||
OnKeyUp(PKBDLLHOOKSTRUCT info) noexcept;
|
||||
|
||||
void MoveSizeStart(HWND window, HMONITOR monitor);
|
||||
void MoveSizeUpdate(HMONITOR monitor, POINT const& ptScreen);
|
||||
@@ -163,6 +312,13 @@ private:
|
||||
void UpdateWorkAreas(bool updateWindowPositions) noexcept;
|
||||
bool ShouldWorkAreasBeRecreated(const std::vector<FancyZonesDataTypes::MonitorId>& monitors, const GUID& virtualDesktop, const std::unordered_map<HMONITOR, std::unique_ptr<WorkArea>>& workAreas) noexcept;
|
||||
void CycleWindows(bool reverse) noexcept;
|
||||
void RotateWindowsAcrossMonitors(bool reverse) noexcept;
|
||||
void ShowMonitorRotationPreview(std::optional<bool> reverse = std::nullopt, bool animateRotation = false) noexcept;
|
||||
void HideMonitorRotationPreview() noexcept;
|
||||
void EnsureMonitorRotationContentNumbers(const std::vector<std::pair<HMONITOR, RECT>>& monitors) noexcept;
|
||||
void RotateMonitorRotationContentNumbers(bool reverse) noexcept;
|
||||
bool IsMonitorRotationActivatorKey(DWORD vkCode) const noexcept;
|
||||
bool IsMonitorRotationChordDown() const noexcept;
|
||||
|
||||
void SyncVirtualDesktops() noexcept;
|
||||
|
||||
@@ -186,6 +342,10 @@ private:
|
||||
WindowKeyboardSnap m_windowKeyboardSnapper{};
|
||||
WorkAreaConfiguration m_workAreaConfiguration;
|
||||
DraggingState m_draggingState;
|
||||
bool m_monitorRotationPreviewActive = false;
|
||||
MonitorRotation::KeyState m_monitorRotationKeyState;
|
||||
std::optional<bool> m_pendingMonitorRotationReverse;
|
||||
std::unordered_map<HMONITOR, size_t> m_monitorRotationContentNumbers;
|
||||
|
||||
wil::unique_handle m_terminateEditorEvent; // Handle of FancyZonesEditor.exe we launch and wait on
|
||||
|
||||
@@ -482,11 +642,50 @@ void FancyZones::WindowCreated(HWND window) noexcept
|
||||
IFACEMETHODIMP_(bool)
|
||||
FancyZones::OnKeyDown(PKBDLLHOOKSTRUCT info) noexcept
|
||||
{
|
||||
m_monitorRotationKeyState.Update(info->vkCode, true);
|
||||
|
||||
// Return true to swallow the keyboard event
|
||||
bool const shift = GetAsyncKeyState(VK_SHIFT) & 0x8000;
|
||||
bool const win = GetAsyncKeyState(VK_LWIN) & 0x8000 || GetAsyncKeyState(VK_RWIN) & 0x8000;
|
||||
bool const alt = GetAsyncKeyState(VK_MENU) & 0x8000;
|
||||
bool const ctrl = GetAsyncKeyState(VK_CONTROL) & 0x8000;
|
||||
if (IsMonitorRotationChordDown())
|
||||
{
|
||||
if (!m_monitorRotationPreviewActive)
|
||||
{
|
||||
m_monitorRotationPreviewActive = true;
|
||||
PostMessageW(m_window, WM_PRIV_MONITOR_ROTATION_PREVIEW_SHOW, 0, 0);
|
||||
}
|
||||
|
||||
if (info->vkCode == VK_LEFT || info->vkCode == VK_RIGHT)
|
||||
{
|
||||
if (!m_pendingMonitorRotationReverse.has_value())
|
||||
{
|
||||
const bool reverse = info->vkCode == VK_LEFT;
|
||||
PostMessageW(m_window, WM_PRIV_MONITOR_ROTATION_PREVIEW_ROTATE, static_cast<WPARAM>(reverse), 0);
|
||||
}
|
||||
|
||||
m_monitorRotationKeyState.Consume(info->vkCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsMonitorRotationActivatorKey(info->vkCode))
|
||||
{
|
||||
m_monitorRotationKeyState.Consume(info->vkCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsRotationPreviewReleaseKey(info->vkCode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (m_monitorRotationPreviewActive)
|
||||
{
|
||||
m_monitorRotationPreviewActive = false;
|
||||
PostMessageW(m_window, WM_PRIV_MONITOR_ROTATION_PREVIEW_HIDE, 0, 0);
|
||||
}
|
||||
|
||||
if ((win && !shift && !ctrl) || (win && ctrl && alt))
|
||||
{
|
||||
if ((info->vkCode == VK_RIGHT) || (info->vkCode == VK_LEFT) || (info->vkCode == VK_UP) || (info->vkCode == VK_DOWN))
|
||||
@@ -541,6 +740,48 @@ FancyZones::OnKeyDown(PKBDLLHOOKSTRUCT info) noexcept
|
||||
return false;
|
||||
}
|
||||
|
||||
IFACEMETHODIMP_(bool)
|
||||
FancyZones::OnKeyUp(PKBDLLHOOKSTRUCT info) noexcept
|
||||
{
|
||||
const bool wasMonitorRotationPreviewActive = m_monitorRotationPreviewActive;
|
||||
const bool isActivatorKey = IsMonitorRotationActivatorKey(info->vkCode);
|
||||
const bool wasConsumed = m_monitorRotationKeyState.ReleaseWasConsumed(info->vkCode);
|
||||
m_monitorRotationKeyState.Update(info->vkCode, false);
|
||||
|
||||
if (wasMonitorRotationPreviewActive && !IsMonitorRotationChordDown())
|
||||
{
|
||||
m_monitorRotationPreviewActive = false;
|
||||
PostMessageW(m_window, WM_PRIV_MONITOR_ROTATION_PREVIEW_HIDE, 0, 0);
|
||||
return isActivatorKey || wasConsumed;
|
||||
}
|
||||
|
||||
return wasConsumed;
|
||||
}
|
||||
|
||||
bool FancyZones::IsMonitorRotationChordDown() const noexcept
|
||||
{
|
||||
if (!FancyZonesSettings::settings().monitorRotation)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& hotkey = FancyZonesSettings::settings().monitorRotationHotkey;
|
||||
const bool winDown = m_monitorRotationKeyState.IsAnyDown({ VK_LWIN, VK_RWIN });
|
||||
const bool ctrlDown = m_monitorRotationKeyState.IsAnyDown({ VK_CONTROL, VK_LCONTROL, VK_RCONTROL });
|
||||
const bool altDown = m_monitorRotationKeyState.IsAnyDown({ VK_MENU, VK_LMENU, VK_RMENU });
|
||||
const bool shiftDown = m_monitorRotationKeyState.IsAnyDown({ VK_SHIFT, VK_LSHIFT, VK_RSHIFT });
|
||||
return hotkey.win_pressed() == winDown &&
|
||||
hotkey.ctrl_pressed() == ctrlDown &&
|
||||
hotkey.alt_pressed() == altDown &&
|
||||
hotkey.shift_pressed() == shiftDown &&
|
||||
m_monitorRotationKeyState.IsDown(hotkey.get_code());
|
||||
}
|
||||
|
||||
bool FancyZones::IsMonitorRotationActivatorKey(DWORD vkCode) const noexcept
|
||||
{
|
||||
return vkCode == FancyZonesSettings::settings().monitorRotationHotkey.get_code();
|
||||
}
|
||||
|
||||
void FancyZones::ToggleEditor() noexcept
|
||||
{
|
||||
_TRACER_;
|
||||
@@ -766,6 +1007,41 @@ LRESULT FancyZones::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lpa
|
||||
{
|
||||
ApplyQuickLayout(static_cast<int>(lparam));
|
||||
}
|
||||
else if (message == WM_PRIV_MONITOR_ROTATION_PREVIEW_SHOW)
|
||||
{
|
||||
ShowMonitorRotationPreview();
|
||||
}
|
||||
else if (message == WM_PRIV_MONITOR_ROTATION_PREVIEW_HIDE)
|
||||
{
|
||||
HideMonitorRotationPreview();
|
||||
}
|
||||
else if (message == WM_PRIV_MONITOR_ROTATION_PREVIEW_ROTATE)
|
||||
{
|
||||
const bool reverse = static_cast<bool>(wparam);
|
||||
m_pendingMonitorRotationReverse = reverse;
|
||||
ShowMonitorRotationPreview(reverse, true);
|
||||
if (SetTimer(m_window, MonitorRotationCommitTimerId, MonitorRotationCommitDelayMillis, nullptr) == 0)
|
||||
{
|
||||
m_pendingMonitorRotationReverse.reset();
|
||||
ShowMonitorRotationPreview();
|
||||
}
|
||||
}
|
||||
else if (message == WM_TIMER && wparam == MonitorRotationCommitTimerId)
|
||||
{
|
||||
KillTimer(m_window, MonitorRotationCommitTimerId);
|
||||
if (m_pendingMonitorRotationReverse.has_value())
|
||||
{
|
||||
const bool reverse = *m_pendingMonitorRotationReverse;
|
||||
const bool shouldShowCommitPreview = m_monitorRotationPreviewActive && IsMonitorRotationChordDown();
|
||||
m_pendingMonitorRotationReverse.reset();
|
||||
RotateWindowsAcrossMonitors(reverse);
|
||||
RotateMonitorRotationContentNumbers(reverse);
|
||||
if (shouldShowCommitPreview)
|
||||
{
|
||||
ShowMonitorRotationPreview();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (message == WM_PRIV_SETTINGS_CHANGED)
|
||||
{
|
||||
FancyZonesSettings::instance().LoadSettings();
|
||||
@@ -1045,6 +1321,172 @@ void FancyZones::CycleWindows(bool reverse) noexcept
|
||||
}
|
||||
}
|
||||
|
||||
void FancyZones::ShowMonitorRotationPreview(std::optional<bool> reverse, bool animateRotation) noexcept
|
||||
{
|
||||
auto windows = CollectWindowRotationSnapshots();
|
||||
std::unordered_map<HMONITOR, std::vector<RECT>> windowRectsByMonitor;
|
||||
for (const auto& window : windows)
|
||||
{
|
||||
windowRectsByMonitor[window.sourceMonitor].push_back(window.sourceRect);
|
||||
}
|
||||
|
||||
auto monitors = FancyZonesUtils::GetAllMonitorRects<&MONITORINFOEX::rcWork>();
|
||||
FancyZonesUtils::OrderMonitors(monitors);
|
||||
EnsureMonitorRotationContentNumbers(monitors);
|
||||
|
||||
if (FancyZonesSettings::settings().spanZonesAcrossMonitors)
|
||||
{
|
||||
auto workArea = m_workAreaConfiguration.GetWorkArea(nullptr);
|
||||
if (workArea)
|
||||
{
|
||||
std::vector<RECT> allWindowRects;
|
||||
allWindowRects.reserve(windows.size());
|
||||
for (const auto& window : windows)
|
||||
{
|
||||
allWindowRects.push_back(window.sourceRect);
|
||||
}
|
||||
|
||||
workArea->ShowMonitorRotationPreview(allWindowRects, 1, reverse, animateRotation);
|
||||
}
|
||||
|
||||
m_monitorRotationPreviewActive = true;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& [monitor, workArea] : m_workAreaConfiguration.GetAllWorkAreas())
|
||||
{
|
||||
if (!workArea)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto iter = windowRectsByMonitor.find(monitor);
|
||||
const std::vector<RECT> emptyRects;
|
||||
const auto monitorNumberIter = m_monitorRotationContentNumbers.find(monitor);
|
||||
const size_t monitorNumber = monitorNumberIter != m_monitorRotationContentNumbers.end() ? monitorNumberIter->second : GetMonitorDisplayNumber(monitor, 1);
|
||||
workArea->ShowMonitorRotationPreview(iter != windowRectsByMonitor.end() ? iter->second : emptyRects, monitorNumber, reverse, animateRotation);
|
||||
}
|
||||
|
||||
m_monitorRotationPreviewActive = true;
|
||||
}
|
||||
|
||||
void FancyZones::EnsureMonitorRotationContentNumbers(const std::vector<std::pair<HMONITOR, RECT>>& monitors) noexcept
|
||||
{
|
||||
if (monitors.empty())
|
||||
{
|
||||
m_monitorRotationContentNumbers.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
bool shouldReinitialize = m_monitorRotationContentNumbers.size() != monitors.size();
|
||||
if (!shouldReinitialize)
|
||||
{
|
||||
for (const auto& [monitor, _] : monitors)
|
||||
{
|
||||
if (!m_monitorRotationContentNumbers.contains(monitor))
|
||||
{
|
||||
shouldReinitialize = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldReinitialize)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_monitorRotationContentNumbers.clear();
|
||||
for (size_t monitorIndex = 0; monitorIndex < monitors.size(); monitorIndex++)
|
||||
{
|
||||
m_monitorRotationContentNumbers[monitors[monitorIndex].first] = GetMonitorDisplayNumber(monitors[monitorIndex].first, monitorIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
void FancyZones::RotateMonitorRotationContentNumbers(bool reverse) noexcept
|
||||
{
|
||||
auto monitors = FancyZonesUtils::GetAllMonitorRects<&MONITORINFOEX::rcWork>();
|
||||
FancyZonesUtils::OrderMonitors(monitors);
|
||||
EnsureMonitorRotationContentNumbers(monitors);
|
||||
|
||||
if (monitors.size() < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::unordered_map<HMONITOR, size_t> rotatedContentNumbers;
|
||||
for (size_t sourceIndex = 0; sourceIndex < monitors.size(); sourceIndex++)
|
||||
{
|
||||
const size_t targetIndex = MonitorRotation::GetRotatedMonitorIndex(sourceIndex, monitors.size(), reverse);
|
||||
rotatedContentNumbers[monitors[targetIndex].first] = m_monitorRotationContentNumbers[monitors[sourceIndex].first];
|
||||
}
|
||||
|
||||
m_monitorRotationContentNumbers = std::move(rotatedContentNumbers);
|
||||
}
|
||||
|
||||
void FancyZones::HideMonitorRotationPreview() noexcept
|
||||
{
|
||||
for (const auto& [_, workArea] : m_workAreaConfiguration.GetAllWorkAreas())
|
||||
{
|
||||
if (workArea)
|
||||
{
|
||||
workArea->HideZones();
|
||||
}
|
||||
}
|
||||
|
||||
m_monitorRotationPreviewActive = false;
|
||||
if (!m_pendingMonitorRotationReverse.has_value())
|
||||
{
|
||||
KillTimer(m_window, MonitorRotationCommitTimerId);
|
||||
}
|
||||
}
|
||||
|
||||
void FancyZones::RotateWindowsAcrossMonitors(bool reverse) noexcept
|
||||
{
|
||||
auto monitors = FancyZonesUtils::GetAllMonitorRects<&MONITORINFOEX::rcWork>();
|
||||
FancyZonesUtils::OrderMonitors(monitors);
|
||||
if (monitors.size() < 2)
|
||||
{
|
||||
Logger::info(L"Monitor rotation skipped: fewer than two monitors are available");
|
||||
return;
|
||||
}
|
||||
|
||||
auto windows = CollectWindowRotationSnapshots();
|
||||
if (windows.empty())
|
||||
{
|
||||
Logger::info(L"Monitor rotation skipped: no processable windows found");
|
||||
return;
|
||||
}
|
||||
|
||||
size_t movedWindows = 0;
|
||||
for (const auto& window : windows)
|
||||
{
|
||||
const auto sourceIndex = FindMonitorIndex(window.sourceMonitor, monitors);
|
||||
if (!sourceIndex.has_value())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const size_t targetIndex = MonitorRotation::GetRotatedMonitorIndex(*sourceIndex, monitors.size(), reverse);
|
||||
const RECT targetRect = MonitorRotation::MapRectBetweenMonitorWorkAreas(
|
||||
window.sourceRect,
|
||||
monitors[*sourceIndex].second,
|
||||
monitors[targetIndex].second);
|
||||
if (!FancyZonesWindowProperties::RetrieveZoneIndexProperty(window.window).empty())
|
||||
{
|
||||
if (auto workArea = m_workAreaConfiguration.GetWorkAreaFromWindow(window.window))
|
||||
{
|
||||
workArea->Unsnap(window.window);
|
||||
}
|
||||
}
|
||||
|
||||
FancyZonesWindowUtils::SizeWindowToRect(window.window, targetRect, false);
|
||||
movedWindows++;
|
||||
}
|
||||
|
||||
Logger::info(L"Rotated {} windows across {} monitors", movedWindows, monitors.size());
|
||||
}
|
||||
|
||||
void FancyZones::SyncVirtualDesktops() noexcept
|
||||
{
|
||||
// Explorer persists current virtual desktop identifier to registry on a per session basis,
|
||||
@@ -1115,6 +1557,18 @@ void FancyZones::SettingsUpdate(SettingId id)
|
||||
UpdateHotkey(static_cast<int>(HotkeyId::NextTab), FancyZonesSettings::settings().nextTabHotkey, FancyZonesSettings::settings().windowSwitching);
|
||||
}
|
||||
break;
|
||||
case SettingId::MonitorRotation:
|
||||
case SettingId::MonitorRotationHotkey:
|
||||
{
|
||||
m_pendingMonitorRotationReverse.reset();
|
||||
KillTimer(m_window, MonitorRotationCommitTimerId);
|
||||
m_monitorRotationKeyState.Reset();
|
||||
if (m_monitorRotationPreviewActive && !IsMonitorRotationChordDown())
|
||||
{
|
||||
HideMonitorRotationPreview();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SettingId::SpanZonesAcrossMonitors:
|
||||
{
|
||||
// See UpdateWorkAreas() — same WindowMouseSnap dangling-WorkArea*
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
#include <common/hooks/WinHookEvent.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <unordered_set>
|
||||
|
||||
struct WinHookEvent;
|
||||
|
||||
@@ -46,6 +49,67 @@ interface __declspec(uuid("{2CB37E8F-87E6-4AEC-B4B2-E0FDC873343F}")) IFancyZones
|
||||
*/
|
||||
IFACEMETHOD_(bool, OnKeyDown)
|
||||
(PKBDLLHOOKSTRUCT info) = 0;
|
||||
/**
|
||||
* Process keyboard key-up event.
|
||||
*
|
||||
* @param info Information about low level keyboard event.
|
||||
* @returns Boolean indicating if this event should be suppressed.
|
||||
*/
|
||||
IFACEMETHOD_(bool, OnKeyUp)
|
||||
(PKBDLLHOOKSTRUCT info) = 0;
|
||||
};
|
||||
|
||||
namespace MonitorRotation
|
||||
{
|
||||
LONG ScaleCoordinate(LONG value, LONG sourceStart, LONG sourceSize, LONG targetStart, LONG targetSize) noexcept;
|
||||
RECT MapRectBetweenMonitorWorkAreas(const RECT& windowRect, const RECT& sourceWorkArea, const RECT& targetWorkArea) noexcept;
|
||||
size_t GetRotatedMonitorIndex(size_t sourceIndex, size_t monitorCount, bool reverse) noexcept;
|
||||
|
||||
class KeyState
|
||||
{
|
||||
public:
|
||||
void Update(DWORD vkCode, bool isDown)
|
||||
{
|
||||
if (isDown)
|
||||
{
|
||||
m_pressedKeys.insert(vkCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_pressedKeys.erase(vkCode);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsDown(DWORD vkCode) const noexcept
|
||||
{
|
||||
return m_pressedKeys.contains(vkCode);
|
||||
}
|
||||
|
||||
bool IsAnyDown(std::initializer_list<DWORD> keys) const noexcept
|
||||
{
|
||||
return std::ranges::any_of(keys, [this](DWORD key) { return IsDown(key); });
|
||||
}
|
||||
|
||||
bool Consume(DWORD vkCode)
|
||||
{
|
||||
return m_consumedKeys.insert(vkCode).second;
|
||||
}
|
||||
|
||||
bool ReleaseWasConsumed(DWORD vkCode)
|
||||
{
|
||||
return m_consumedKeys.erase(vkCode) != 0;
|
||||
}
|
||||
|
||||
void Reset() noexcept
|
||||
{
|
||||
m_pressedKeys.clear();
|
||||
m_consumedKeys.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_set<DWORD> m_pressedKeys;
|
||||
std::unordered_set<DWORD> m_consumedKeys;
|
||||
};
|
||||
}
|
||||
|
||||
winrt::com_ptr<IFancyZones> MakeFancyZones(HINSTANCE hinstance, std::function<void()> disableCallback) noexcept;
|
||||
|
||||
@@ -20,6 +20,9 @@ UINT WM_PRIV_SNAP_HOTKEY;
|
||||
UINT WM_PRIV_QUICK_LAYOUT_KEY;
|
||||
UINT WM_PRIV_SETTINGS_CHANGED;
|
||||
UINT WM_PRIV_SAVE_EDITOR_PARAMETERS;
|
||||
UINT WM_PRIV_MONITOR_ROTATION_PREVIEW_SHOW;
|
||||
UINT WM_PRIV_MONITOR_ROTATION_PREVIEW_HIDE;
|
||||
UINT WM_PRIV_MONITOR_ROTATION_PREVIEW_ROTATE;
|
||||
|
||||
std::once_flag init_flag;
|
||||
|
||||
@@ -44,5 +47,8 @@ void InitializeWinhookEventIds()
|
||||
WM_PRIV_QUICK_LAYOUT_KEY = RegisterWindowMessage(L"{15baab3d-c67b-4a15-aFF0-13610e05e947}");
|
||||
WM_PRIV_SETTINGS_CHANGED = RegisterWindowMessage(L"{89ca3Daa-bf2d-4e73-9f3f-c60716364e27}");
|
||||
WM_PRIV_SAVE_EDITOR_PARAMETERS = RegisterWindowMessage(L"{d8f9c0e3-5d77-4e83-8a4f-7c704c2bfb4a}");
|
||||
WM_PRIV_MONITOR_ROTATION_PREVIEW_SHOW = RegisterWindowMessage(L"{98426d34-6138-4613-ad61-34ad0294ff15}");
|
||||
WM_PRIV_MONITOR_ROTATION_PREVIEW_HIDE = RegisterWindowMessage(L"{0598cbfd-908c-4c72-802a-03da9bdff7c8}");
|
||||
WM_PRIV_MONITOR_ROTATION_PREVIEW_ROTATE = RegisterWindowMessage(L"{ea0e8fc6-161a-49fd-bbe4-441fd956236a}");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,5 +18,8 @@ extern UINT WM_PRIV_SNAP_HOTKEY; // Scheduled when we receive a snap hotkey key
|
||||
extern UINT WM_PRIV_QUICK_LAYOUT_KEY; // Scheduled when we receive a key down press to quickly apply a layout
|
||||
extern UINT WM_PRIV_SETTINGS_CHANGED; // Scheduled when a watched settings file is updated
|
||||
extern UINT WM_PRIV_SAVE_EDITOR_PARAMETERS; // Scheduled to request saving editor-parameters.json
|
||||
extern UINT WM_PRIV_MONITOR_ROTATION_PREVIEW_SHOW; // Scheduled when the monitor rotation preview should be shown
|
||||
extern UINT WM_PRIV_MONITOR_ROTATION_PREVIEW_HIDE; // Scheduled when the monitor rotation preview should be hidden
|
||||
extern UINT WM_PRIV_MONITOR_ROTATION_PREVIEW_ROTATE; // Scheduled when monitor rotation preview should rotate windows
|
||||
|
||||
void InitializeWinhookEventIds();
|
||||
|
||||
@@ -45,6 +45,8 @@ namespace NonLocalizable
|
||||
const wchar_t WindowSwitchingToggleID[] = L"fancyzones_windowSwitching";
|
||||
const wchar_t NextTabHotkeyID[] = L"fancyzones_nextTab_hotkey";
|
||||
const wchar_t PrevTabHotkeyID[] = L"fancyzones_prevTab_hotkey";
|
||||
const wchar_t MonitorRotationID[] = L"fancyzones_monitorRotation";
|
||||
const wchar_t MonitorRotationHotkeyID[] = L"fancyzones_monitorRotation_hotkey";
|
||||
const wchar_t ExcludedAppsID[] = L"fancyzones_excluded_apps";
|
||||
const wchar_t ZoneHighlightOpacityID[] = L"fancyzones_highlight_opacity";
|
||||
const wchar_t ShowZoneNumberID[] = L"fancyzones_showZoneNumber";
|
||||
@@ -124,6 +126,7 @@ void FancyZonesSettings::LoadSettings()
|
||||
SetBoolFlag(values, NonLocalizable::SpanZonesAcrossMonitorsID, SettingId::SpanZonesAcrossMonitors, m_settings.spanZonesAcrossMonitors);
|
||||
SetBoolFlag(values, NonLocalizable::MakeDraggedWindowTransparentID, SettingId::MakeDraggedWindowsTransparent, m_settings.makeDraggedWindowTransparent);
|
||||
SetBoolFlag(values, NonLocalizable::WindowSwitchingToggleID, SettingId::WindowSwitching, m_settings.windowSwitching);
|
||||
SetBoolFlag(values, NonLocalizable::MonitorRotationID, SettingId::MonitorRotation, m_settings.monitorRotation);
|
||||
SetBoolFlag(values, NonLocalizable::SystemThemeID, SettingId::SystemTheme, m_settings.systemTheme);
|
||||
SetBoolFlag(values, NonLocalizable::ShowZoneNumberID, SettingId::ShowZoneNumber, m_settings.showZoneNumber);
|
||||
SetBoolFlag(values, NonLocalizable::AllowChildWindowSnapID, SettingId::AllowSnapChildWindows, m_settings.allowSnapChildWindows);
|
||||
@@ -206,6 +209,16 @@ void FancyZonesSettings::LoadSettings()
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto val = values.get_json(NonLocalizable::MonitorRotationHotkeyID))
|
||||
{
|
||||
auto hotkey = PowerToysSettings::HotkeyObject::from_json(*val);
|
||||
if (m_settings.monitorRotationHotkey.get_modifiers() != hotkey.get_modifiers() || m_settings.monitorRotationHotkey.get_key() != hotkey.get_key() || m_settings.monitorRotationHotkey.get_code() != hotkey.get_code())
|
||||
{
|
||||
m_settings.monitorRotationHotkey = PowerToysSettings::HotkeyObject::from_json(*val);
|
||||
NotifyObservers(SettingId::MonitorRotationHotkey);
|
||||
}
|
||||
}
|
||||
|
||||
// excluded apps
|
||||
if (auto val = values.get_string_value(NonLocalizable::ExcludedAppsID))
|
||||
{
|
||||
|
||||
@@ -56,6 +56,8 @@ struct Settings
|
||||
bool windowSwitching = true;
|
||||
PowerToysSettings::HotkeyObject nextTabHotkey = PowerToysSettings::HotkeyObject::from_settings(true, false, false, false, VK_NEXT);
|
||||
PowerToysSettings::HotkeyObject prevTabHotkey = PowerToysSettings::HotkeyObject::from_settings(true, false, false, false, VK_PRIOR);
|
||||
bool monitorRotation = false;
|
||||
PowerToysSettings::HotkeyObject monitorRotationHotkey = PowerToysSettings::HotkeyObject::from_settings(false, false, true, false, 'X');
|
||||
std::wstring excludedApps = L"";
|
||||
std::vector<std::wstring> excludedAppsArray;
|
||||
};
|
||||
|
||||
@@ -31,7 +31,9 @@ enum class SettingId
|
||||
WindowSwitching,
|
||||
NextTabHotkey,
|
||||
PrevTabHotkey,
|
||||
MonitorRotation,
|
||||
MonitorRotationHotkey,
|
||||
ExcludedApps,
|
||||
AllowSnapChildWindows,
|
||||
DisableRoundCornersOnSnapping,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -205,6 +205,30 @@ void WorkArea::FlashZones()
|
||||
}
|
||||
}
|
||||
|
||||
void WorkArea::ShowMonitorRotationPreview(const std::vector<RECT>& windowRects, size_t monitorNumber, std::optional<bool> reverse, bool animateRotation)
|
||||
{
|
||||
if (!m_zonesOverlay)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<RECT> localWindowRects;
|
||||
localWindowRects.reserve(windowRects.size());
|
||||
for (const auto& rect : windowRects)
|
||||
{
|
||||
localWindowRects.push_back(RECT{
|
||||
.left = rect.left - m_workAreaRect.left(),
|
||||
.top = rect.top - m_workAreaRect.top(),
|
||||
.right = rect.right - m_workAreaRect.left(),
|
||||
.bottom = rect.bottom - m_workAreaRect.top(),
|
||||
});
|
||||
}
|
||||
|
||||
SetWorkAreaWindowAsTopmost(nullptr);
|
||||
m_zonesOverlay->DrawMonitorRotationPreview(localWindowRects, monitorNumber, reverse, animateRotation);
|
||||
m_zonesOverlay->Show();
|
||||
}
|
||||
|
||||
void WorkArea::InitLayout()
|
||||
{
|
||||
InitLayout({});
|
||||
@@ -379,4 +403,4 @@ LRESULT CALLBACK WorkArea::s_WndProc(HWND window, UINT message, WPARAM wparam, L
|
||||
|
||||
return (thisRef != nullptr) ? thisRef->WndProc(message, wparam, lparam) :
|
||||
DefWindowProc(window, message, wparam, lparam);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <FancyZonesLib/Layout.h>
|
||||
#include <FancyZonesLib/LayoutAssignedWindows.h>
|
||||
|
||||
@@ -53,6 +56,7 @@ public:
|
||||
void ShowZones(const ZoneIndexSet& highlight, HWND draggedWindow = nullptr);
|
||||
void HideZones();
|
||||
void FlashZones();
|
||||
void ShowMonitorRotationPreview(const std::vector<RECT>& windowRects, size_t monitorNumber, std::optional<bool> reverse, bool animateRotation);
|
||||
|
||||
void CycleWindows(HWND window, bool reverse);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "ZonesOverlay.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -13,6 +14,83 @@ namespace
|
||||
{
|
||||
const int FadeInDurationMillis = 200;
|
||||
const int FlashZonesDurationMillis = 700;
|
||||
const int RotationPulseDurationMillis = 920;
|
||||
|
||||
D2D1_RECT_F ShrinkRect(D2D1_RECT_F rect, float scale)
|
||||
{
|
||||
const float width = rect.right - rect.left;
|
||||
const float height = rect.bottom - rect.top;
|
||||
const float dx = width * (1.f - scale) / 2.f;
|
||||
const float dy = height * (1.f - scale) / 2.f;
|
||||
|
||||
return D2D1::RectF(rect.left + dx, rect.top + dy, rect.right - dx, rect.bottom - dy);
|
||||
}
|
||||
|
||||
constexpr float ClampDimension(float value, float min, float max) noexcept
|
||||
{
|
||||
return std::clamp(value, min, max);
|
||||
}
|
||||
|
||||
float TriangleWave(float value) noexcept
|
||||
{
|
||||
value -= std::floor(value);
|
||||
return value < 0.5f ? value * 2.f : (1.f - value) * 2.f;
|
||||
}
|
||||
|
||||
constexpr float SmoothStep(float value) noexcept
|
||||
{
|
||||
value = std::clamp(value, 0.f, 1.f);
|
||||
return value * value * (3.f - 2.f * value);
|
||||
}
|
||||
|
||||
D2D1_COLOR_F MonitorAccentColor(size_t monitorNumber, float alpha = 1.f) noexcept
|
||||
{
|
||||
switch ((monitorNumber - 1) % 6)
|
||||
{
|
||||
case 0:
|
||||
return D2D1::ColorF(0.38f, 0.76f, 1.f, alpha);
|
||||
case 1:
|
||||
return D2D1::ColorF(0.45f, 0.91f, 0.58f, alpha);
|
||||
case 2:
|
||||
return D2D1::ColorF(1.f, 0.70f, 0.28f, alpha);
|
||||
case 3:
|
||||
return D2D1::ColorF(0.86f, 0.56f, 1.f, alpha);
|
||||
case 4:
|
||||
return D2D1::ColorF(0.34f, 0.93f, 0.88f, alpha);
|
||||
default:
|
||||
return D2D1::ColorF(1.f, 0.48f, 0.55f, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawArrowLine(ID2D1RenderTarget* renderTarget, ID2D1Brush* brush, D2D1_POINT_2F start, D2D1_POINT_2F end, float strokeWidth) noexcept
|
||||
{
|
||||
renderTarget->DrawLine(start, end, brush, strokeWidth);
|
||||
|
||||
const float dx = end.x - start.x;
|
||||
const float dy = end.y - start.y;
|
||||
const float length = std::sqrt(dx * dx + dy * dy);
|
||||
if (length <= 0.001f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const float unitX = dx / length;
|
||||
const float unitY = dy / length;
|
||||
const float normalX = -unitY;
|
||||
const float normalY = unitX;
|
||||
const float headLength = strokeWidth * 4.2f;
|
||||
const float headSpread = strokeWidth * 2.7f;
|
||||
const auto headA = D2D1::Point2F(end.x - unitX * headLength + normalX * headSpread, end.y - unitY * headLength + normalY * headSpread);
|
||||
const auto headB = D2D1::Point2F(end.x - unitX * headLength - normalX * headSpread, end.y - unitY * headLength - normalY * headSpread);
|
||||
|
||||
renderTarget->DrawLine(end, headA, brush, strokeWidth);
|
||||
renderTarget->DrawLine(end, headB, brush, strokeWidth);
|
||||
}
|
||||
|
||||
D2D1_RECT_F CenteredRect(float centerX, float centerY, float width, float height) noexcept
|
||||
{
|
||||
return D2D1::RectF(centerX - width / 2.f, centerY - height / 2.f, centerX + width / 2.f, centerY + height / 2.f);
|
||||
}
|
||||
}
|
||||
|
||||
namespace NonLocalizable
|
||||
@@ -64,6 +142,11 @@ D2D1_RECT_F ZonesOverlay::ConvertRect(RECT rect)
|
||||
return D2D1::RectF(rect.left + 0.5f, rect.top + 0.5f, rect.right - 0.5f, rect.bottom - 0.5f);
|
||||
}
|
||||
|
||||
D2D1_RECT_F ZonesOverlay::OffsetRect(D2D1_RECT_F rect, float x, float y)
|
||||
{
|
||||
return D2D1::RectF(rect.left + x, rect.top + y, rect.right + x, rect.bottom + y);
|
||||
}
|
||||
|
||||
ZonesOverlay::ZonesOverlay(HWND window)
|
||||
{
|
||||
HRESULT hr;
|
||||
@@ -125,11 +208,47 @@ ZonesOverlay::RenderResult ZonesOverlay::Render()
|
||||
{
|
||||
animationAlpha = 1.f;
|
||||
}
|
||||
const bool animateRotation = m_animateRotation && isEnabledAnimations;
|
||||
|
||||
m_renderTarget->BeginDraw();
|
||||
|
||||
const auto renderNow = std::chrono::steady_clock().now();
|
||||
float rotationPulse = 0.f;
|
||||
float rotationProgress = 0.f;
|
||||
if (animateRotation && m_rotationPulseStart)
|
||||
{
|
||||
const auto pulseMillis = (renderNow - *m_rotationPulseStart).count() / 1e6f;
|
||||
if (pulseMillis < RotationPulseDurationMillis)
|
||||
{
|
||||
rotationProgress = std::clamp(pulseMillis / RotationPulseDurationMillis, 0.f, 1.f);
|
||||
rotationPulse = 1.f - rotationProgress;
|
||||
}
|
||||
else
|
||||
{
|
||||
rotationProgress = 1.f;
|
||||
m_rotationPulseStart.reset();
|
||||
}
|
||||
}
|
||||
|
||||
const auto width = static_cast<float>(m_clientRect.right - m_clientRect.left);
|
||||
const auto height = static_cast<float>(m_clientRect.bottom - m_clientRect.top);
|
||||
const float directionSign = m_rotationDirection == RotationDirection::Left ? -1.f : (m_rotationDirection == RotationDirection::Right ? 1.f : 0.f);
|
||||
const float easedRotationProgress = SmoothStep(rotationProgress);
|
||||
const float pulseOffset = animateRotation ? directionSign * easedRotationProgress * ClampDimension(width * 0.075f, 44.f, 132.f) : 0.f;
|
||||
const float pulseScale = 1.f + ((animateRotation ? rotationPulse : 0.f) * 0.05f);
|
||||
|
||||
// Draw backdrop
|
||||
m_renderTarget->Clear(D2D1::ColorF(0.f, 0.f, 0.f, 0.f));
|
||||
if (m_drawBackdrop)
|
||||
{
|
||||
ID2D1SolidColorBrush* backdropBrush = nullptr;
|
||||
m_renderTarget->CreateSolidColorBrush(D2D1::ColorF(0.f, 0.f, 0.f, 0.88f * animationAlpha), &backdropBrush);
|
||||
if (backdropBrush)
|
||||
{
|
||||
m_renderTarget->FillRectangle(ConvertRect(m_clientRect), backdropBrush);
|
||||
backdropBrush->Release();
|
||||
}
|
||||
}
|
||||
|
||||
IDWriteTextFormat* textFormat = nullptr;
|
||||
|
||||
@@ -142,13 +261,13 @@ ZonesOverlay::RenderResult ZonesOverlay::Render()
|
||||
|
||||
for (auto drawableRect : m_sceneRects)
|
||||
{
|
||||
ID2D1SolidColorBrush* textBrush = nullptr;
|
||||
ID2D1SolidColorBrush* borderBrush = nullptr;
|
||||
ID2D1SolidColorBrush* fillBrush = nullptr;
|
||||
|
||||
// Need to copy the rect from m_sceneRects
|
||||
drawableRect.borderColor.a *= animationAlpha;
|
||||
drawableRect.fillColor.a *= animationAlpha;
|
||||
drawableRect.rect = OffsetRect(drawableRect.rect, pulseOffset, 0.f);
|
||||
|
||||
m_renderTarget->CreateSolidColorBrush(drawableRect.borderColor, &borderBrush);
|
||||
m_renderTarget->CreateSolidColorBrush(drawableRect.fillColor, &fillBrush);
|
||||
@@ -169,6 +288,7 @@ ZonesOverlay::RenderResult ZonesOverlay::Render()
|
||||
|
||||
if (drawableRect.showText)
|
||||
{
|
||||
ID2D1SolidColorBrush* textBrush = nullptr;
|
||||
m_renderTarget->CreateSolidColorBrush(drawableRect.textColor, &textBrush);
|
||||
|
||||
if (textFormat && textBrush)
|
||||
@@ -190,6 +310,153 @@ ZonesOverlay::RenderResult ZonesOverlay::Render()
|
||||
textFormat->Release();
|
||||
}
|
||||
|
||||
const auto accentColor = MonitorAccentColor(m_monitorNumber.value_or(1));
|
||||
|
||||
if (animateRotation && (m_rotationDirection == RotationDirection::Left || m_rotationDirection == RotationDirection::Right))
|
||||
{
|
||||
constexpr int MarkerCount = 7;
|
||||
const float railWidth = ClampDimension(width * 0.58f, 260.f, 760.f);
|
||||
const float railLeft = (width - railWidth) / 2.f;
|
||||
const float railY = (height / 2.f) + ClampDimension(height * 0.20f, 104.f, 222.f);
|
||||
const float segmentWidth = railWidth / MarkerCount;
|
||||
const float markerSize = ClampDimension(width * 0.025f, 18.f, 34.f);
|
||||
const float strokeWidth = ClampDimension(width * 0.0038f, 3.4f, 6.4f);
|
||||
|
||||
for (int markerIndex = 0; markerIndex < MarkerCount; markerIndex++)
|
||||
{
|
||||
const float order = m_rotationDirection == RotationDirection::Left ? static_cast<float>(MarkerCount - 1 - markerIndex) : static_cast<float>(markerIndex);
|
||||
const float wave = TriangleWave(rotationProgress * 2.4f - order * 0.14f);
|
||||
const float alpha = (0.18f + wave * 0.64f) * animationAlpha;
|
||||
ID2D1SolidColorBrush* markerBrush = nullptr;
|
||||
m_renderTarget->CreateSolidColorBrush(D2D1::ColorF(accentColor.r, accentColor.g, accentColor.b, alpha), &markerBrush);
|
||||
|
||||
if (markerBrush)
|
||||
{
|
||||
const float centerX = railLeft + segmentWidth * (markerIndex + 0.5f) + directionSign * wave * 9.f;
|
||||
const float headX = centerX + directionSign * markerSize;
|
||||
const auto head = D2D1::Point2F(headX, railY);
|
||||
const auto wingTop = D2D1::Point2F(centerX - directionSign * markerSize * 0.45f, railY - markerSize * 0.62f);
|
||||
const auto wingBottom = D2D1::Point2F(centerX - directionSign * markerSize * 0.45f, railY + markerSize * 0.62f);
|
||||
m_renderTarget->DrawLine(wingTop, head, markerBrush, strokeWidth);
|
||||
m_renderTarget->DrawLine(wingBottom, head, markerBrush, strokeWidth);
|
||||
markerBrush->Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_monitorNumber && writeFactory)
|
||||
{
|
||||
const float smallerDimension = width < height ? width : height;
|
||||
const bool isRotating = animateRotation && (m_rotationDirection == RotationDirection::Left || m_rotationDirection == RotationDirection::Right);
|
||||
const float radius = ClampDimension(smallerDimension * 0.14f, 82.f, 170.f) * pulseScale;
|
||||
const float centerX = width / 2.f;
|
||||
const float centerY = height / 2.f;
|
||||
|
||||
if (isRotating)
|
||||
{
|
||||
ID2D1SolidColorBrush* ghostFillBrush = nullptr;
|
||||
ID2D1SolidColorBrush* ghostBorderBrush = nullptr;
|
||||
m_renderTarget->CreateSolidColorBrush(D2D1::ColorF(accentColor.r, accentColor.g, accentColor.b, 0.10f * animationAlpha), &ghostFillBrush);
|
||||
m_renderTarget->CreateSolidColorBrush(D2D1::ColorF(accentColor.r, accentColor.g, accentColor.b, 0.42f * animationAlpha), &ghostBorderBrush);
|
||||
|
||||
if (ghostFillBrush && ghostBorderBrush)
|
||||
{
|
||||
const float ghostWidth = ClampDimension(width * 0.34f, 220.f, 520.f);
|
||||
const float ghostHeight = ClampDimension(height * 0.17f, 110.f, 220.f);
|
||||
const float ghostCenterX = (width / 2.f) + pulseOffset;
|
||||
const float ghostCenterY = centerY - ClampDimension(height * 0.17f, 72.f, 170.f);
|
||||
const auto ghostRect = CenteredRect(ghostCenterX, ghostCenterY, ghostWidth, ghostHeight);
|
||||
const float cornerRadius = ClampDimension(ghostHeight * 0.16f, 14.f, 28.f);
|
||||
const auto roundedGhostRect = D2D1::RoundedRect(ghostRect, cornerRadius, cornerRadius);
|
||||
m_renderTarget->FillRoundedRectangle(roundedGhostRect, ghostFillBrush);
|
||||
m_renderTarget->DrawRoundedRectangle(roundedGhostRect, ghostBorderBrush, ClampDimension(width * 0.002f, 2.f, 4.f));
|
||||
|
||||
const int innerLines = 3;
|
||||
const float lineGap = ghostHeight * 0.20f;
|
||||
const float lineStart = ghostCenterY - lineGap;
|
||||
for (int lineIndex = 0; lineIndex < innerLines; lineIndex++)
|
||||
{
|
||||
const float lineY = lineStart + lineGap * lineIndex;
|
||||
const float lineInset = ghostWidth * (0.18f + lineIndex * 0.04f);
|
||||
m_renderTarget->DrawLine(
|
||||
D2D1::Point2F(ghostRect.left + lineInset, lineY),
|
||||
D2D1::Point2F(ghostRect.right - lineInset, lineY),
|
||||
ghostBorderBrush,
|
||||
ClampDimension(width * 0.0014f, 1.5f, 3.f));
|
||||
}
|
||||
}
|
||||
|
||||
if (ghostFillBrush)
|
||||
{
|
||||
ghostFillBrush->Release();
|
||||
}
|
||||
|
||||
if (ghostBorderBrush)
|
||||
{
|
||||
ghostBorderBrush->Release();
|
||||
}
|
||||
}
|
||||
|
||||
ID2D1SolidColorBrush* glowBrush = nullptr;
|
||||
ID2D1SolidColorBrush* numberBrush = nullptr;
|
||||
m_renderTarget->CreateSolidColorBrush(D2D1::ColorF(accentColor.r, accentColor.g, accentColor.b, (0.11f + rotationPulse * 0.11f) * animationAlpha), &glowBrush);
|
||||
m_renderTarget->CreateSolidColorBrush(D2D1::ColorF(1.f, 1.f, 1.f, 0.95f * animationAlpha), &numberBrush);
|
||||
|
||||
if (glowBrush)
|
||||
{
|
||||
m_renderTarget->FillEllipse(D2D1::Ellipse(D2D1::Point2F(centerX, centerY), radius + 22.f, radius + 22.f), glowBrush);
|
||||
glowBrush->Release();
|
||||
}
|
||||
|
||||
IDWriteTextFormat* numberFormat = nullptr;
|
||||
writeFactory->CreateTextFormat(NonLocalizable::SegoeUiFont, nullptr, DWRITE_FONT_WEIGHT_BOLD, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, isRotating ? radius * 0.72f : radius * 1.15f, L"en-US", &numberFormat);
|
||||
if (numberFormat && numberBrush)
|
||||
{
|
||||
const auto numberStr = std::to_wstring(*m_monitorNumber);
|
||||
numberFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
|
||||
numberFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
|
||||
const auto numberRect = isRotating ?
|
||||
D2D1::RectF(centerX - (radius * 0.45f), centerY - (radius * 0.45f), centerX + (radius * 0.45f), centerY + (radius * 0.45f)) :
|
||||
D2D1::RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
|
||||
m_renderTarget->DrawTextW(numberStr.c_str(), static_cast<UINT32>(numberStr.size()), numberFormat, numberRect, numberBrush);
|
||||
}
|
||||
|
||||
if (isRotating)
|
||||
{
|
||||
ID2D1SolidColorBrush* symbolBrush = nullptr;
|
||||
m_renderTarget->CreateSolidColorBrush(D2D1::ColorF(accentColor.r, accentColor.g, accentColor.b, (0.84f + rotationPulse * 0.14f) * animationAlpha), &symbolBrush);
|
||||
|
||||
if (symbolBrush)
|
||||
{
|
||||
const float strokeWidth = ClampDimension(radius * 0.045f, 4.5f, 8.5f);
|
||||
const float arm = radius * 0.76f;
|
||||
const float gap = radius * 0.30f;
|
||||
if (m_rotationDirection == RotationDirection::Left)
|
||||
{
|
||||
DrawArrowLine(m_renderTarget, symbolBrush, D2D1::Point2F(centerX + gap, centerY - gap), D2D1::Point2F(centerX - arm, centerY - arm), strokeWidth);
|
||||
DrawArrowLine(m_renderTarget, symbolBrush, D2D1::Point2F(centerX - gap, centerY + gap), D2D1::Point2F(centerX + arm, centerY + arm), strokeWidth);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawArrowLine(m_renderTarget, symbolBrush, D2D1::Point2F(centerX - gap, centerY - gap), D2D1::Point2F(centerX + arm, centerY - arm), strokeWidth);
|
||||
DrawArrowLine(m_renderTarget, symbolBrush, D2D1::Point2F(centerX + gap, centerY + gap), D2D1::Point2F(centerX - arm, centerY + arm), strokeWidth);
|
||||
}
|
||||
|
||||
symbolBrush->Release();
|
||||
}
|
||||
}
|
||||
|
||||
if (numberBrush)
|
||||
{
|
||||
numberBrush->Release();
|
||||
}
|
||||
|
||||
if (numberFormat)
|
||||
{
|
||||
numberFormat->Release();
|
||||
}
|
||||
}
|
||||
|
||||
// The lock must be released here, as EndDraw() will wait for vertical sync
|
||||
lock.unlock();
|
||||
|
||||
@@ -286,6 +553,11 @@ void ZonesOverlay::DrawActiveZoneSet(const ZonesMap& zones,
|
||||
std::unique_lock lock(m_mutex);
|
||||
|
||||
m_sceneRects = {};
|
||||
m_drawBackdrop = false;
|
||||
m_rotationDirection = RotationDirection::None;
|
||||
m_animateRotation = false;
|
||||
m_monitorNumber.reset();
|
||||
m_rotationPulseStart.reset();
|
||||
|
||||
auto borderColor = ConvertColor(colors.borderColor);
|
||||
auto inactiveColor = ConvertColor(colors.primaryColor);
|
||||
@@ -338,6 +610,47 @@ void ZonesOverlay::DrawActiveZoneSet(const ZonesMap& zones,
|
||||
}
|
||||
}
|
||||
|
||||
void ZonesOverlay::DrawMonitorRotationPreview(const std::vector<RECT>& windowRects, size_t monitorNumber, std::optional<bool> reverse, bool animateRotation)
|
||||
{
|
||||
std::unique_lock lock(m_mutex);
|
||||
|
||||
m_sceneRects = {};
|
||||
m_drawBackdrop = true;
|
||||
m_rotationDirection = reverse.has_value() ? (reverse.value() ? RotationDirection::Left : RotationDirection::Right) : RotationDirection::Both;
|
||||
m_animateRotation = animateRotation && reverse.has_value();
|
||||
m_monitorNumber = monitorNumber;
|
||||
if (m_animateRotation)
|
||||
{
|
||||
m_rotationPulseStart = std::chrono::steady_clock().now();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_rotationPulseStart.reset();
|
||||
}
|
||||
|
||||
const auto borderColor = D2D1::ColorF(1.f, 1.f, 1.f, 0.08f);
|
||||
const auto fillColor = D2D1::ColorF(0.05f, 0.07f, 0.09f, 0.50f);
|
||||
const auto textColor = D2D1::ColorF(1.f, 1.f, 1.f, 0.85f);
|
||||
|
||||
for (const auto& rect : windowRects)
|
||||
{
|
||||
const auto drawableRect = ShrinkRect(ConvertRect(rect), 0.82f);
|
||||
if (drawableRect.right - drawableRect.left < 18.f || drawableRect.bottom - drawableRect.top < 18.f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
m_sceneRects.push_back(DrawableRect{
|
||||
.rect = drawableRect,
|
||||
.borderColor = borderColor,
|
||||
.fillColor = fillColor,
|
||||
.textColor = textColor,
|
||||
.id = 0,
|
||||
.showText = false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ZonesOverlay::~ZonesOverlay()
|
||||
{
|
||||
// Constructor early-returns (e.g. CreateHwndRenderTarget failing during a
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include <wil\resource.h>
|
||||
#include <winrt/base.h>
|
||||
@@ -31,6 +32,14 @@ class ZonesOverlay
|
||||
bool autoHide;
|
||||
};
|
||||
|
||||
enum class RotationDirection
|
||||
{
|
||||
None,
|
||||
Left,
|
||||
Right,
|
||||
Both,
|
||||
};
|
||||
|
||||
enum struct RenderResult
|
||||
{
|
||||
Ok,
|
||||
@@ -45,11 +54,17 @@ class ZonesOverlay
|
||||
|
||||
std::mutex m_mutex;
|
||||
std::vector<DrawableRect> m_sceneRects;
|
||||
bool m_drawBackdrop = false;
|
||||
RotationDirection m_rotationDirection = RotationDirection::None;
|
||||
bool m_animateRotation = false;
|
||||
std::optional<size_t> m_monitorNumber;
|
||||
std::optional<std::chrono::steady_clock::time_point> m_rotationPulseStart;
|
||||
|
||||
float GetAnimationAlpha();
|
||||
static IDWriteFactory* GetWriteFactory();
|
||||
static D2D1_COLOR_F ConvertColor(COLORREF color);
|
||||
static D2D1_RECT_F ConvertRect(RECT rect);
|
||||
static D2D1_RECT_F OffsetRect(D2D1_RECT_F rect, float x, float y);
|
||||
RenderResult Render();
|
||||
void RenderLoop();
|
||||
|
||||
@@ -69,4 +84,5 @@ public:
|
||||
const ZoneIndexSet& highlightZones,
|
||||
const Colors::ZoneColors& colors,
|
||||
const bool showZoneText);
|
||||
void DrawMonitorRotationPreview(const std::vector<RECT>& windowRects, size_t monitorNumber, std::optional<bool> reverse, bool animateRotation);
|
||||
};
|
||||
|
||||
@@ -43,6 +43,7 @@ namespace FancyZonesUnitTests
|
||||
Assert::AreEqual(expected.spanZonesAcrossMonitors, actual.spanZonesAcrossMonitors);
|
||||
Assert::AreEqual(expected.makeDraggedWindowTransparent, actual.makeDraggedWindowTransparent);
|
||||
Assert::AreEqual(expected.windowSwitching, actual.windowSwitching);
|
||||
Assert::AreEqual(expected.monitorRotation, actual.monitorRotation);
|
||||
Assert::AreEqual(expected.zoneColor.c_str(), actual.zoneColor.c_str());
|
||||
Assert::AreEqual(expected.zoneBorderColor.c_str(), actual.zoneBorderColor.c_str());
|
||||
Assert::AreEqual(expected.zoneHighlightColor.c_str(), actual.zoneHighlightColor.c_str());
|
||||
@@ -57,6 +58,7 @@ namespace FancyZonesUnitTests
|
||||
compareHotkeyObjects(expected.editorHotkey, actual.editorHotkey);
|
||||
compareHotkeyObjects(expected.nextTabHotkey, actual.nextTabHotkey);
|
||||
compareHotkeyObjects(expected.prevTabHotkey, actual.prevTabHotkey);
|
||||
compareHotkeyObjects(expected.monitorRotationHotkey, actual.monitorRotationHotkey);
|
||||
}
|
||||
|
||||
TEST_CLASS (FancyZonesSettingsUnitTest)
|
||||
@@ -90,6 +92,8 @@ namespace FancyZonesUnitTests
|
||||
values.add_property(L"fancyzones_windowSwitching", m_defaultSettings.windowSwitching);
|
||||
values.add_property(L"fancyzones_nextTab_hotkey", m_defaultSettings.nextTabHotkey.get_json());
|
||||
values.add_property(L"fancyzones_prevTab_hotkey", m_defaultSettings.prevTabHotkey.get_json());
|
||||
values.add_property(L"fancyzones_monitorRotation", m_defaultSettings.monitorRotation);
|
||||
values.add_property(L"fancyzones_monitorRotation_hotkey", m_defaultSettings.monitorRotationHotkey.get_json());
|
||||
values.add_property(L"fancyzones_excluded_apps", m_defaultSettings.excludedApps);
|
||||
|
||||
json::to_file(FancyZonesSettings::GetSettingsFileName(), values.get_raw_json());
|
||||
@@ -133,6 +137,8 @@ namespace FancyZonesUnitTests
|
||||
values.add_property(L"fancyzones_windowSwitching", expected.windowSwitching);
|
||||
values.add_property(L"fancyzones_nextTab_hotkey", expected.nextTabHotkey.get_json());
|
||||
values.add_property(L"fancyzones_prevTab_hotkey", expected.prevTabHotkey.get_json());
|
||||
values.add_property(L"fancyzones_monitorRotation", expected.monitorRotation);
|
||||
values.add_property(L"fancyzones_monitorRotation_hotkey", expected.monitorRotationHotkey.get_json());
|
||||
values.add_property(L"fancyzones_excluded_apps", expected.excludedApps);
|
||||
|
||||
json::to_file(FancyZonesSettings::GetSettingsFileName(), values.get_raw_json());
|
||||
@@ -161,4 +167,82 @@ namespace FancyZonesUnitTests
|
||||
compareSettings(m_defaultSettings, actual);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS (MonitorRotationUnitTest)
|
||||
{
|
||||
TEST_METHOD (RotatedMonitorIndexWrapsInBothDirections)
|
||||
{
|
||||
Assert::AreEqual<size_t>(1, MonitorRotation::GetRotatedMonitorIndex(0, 3, false));
|
||||
Assert::AreEqual<size_t>(0, MonitorRotation::GetRotatedMonitorIndex(2, 3, false));
|
||||
Assert::AreEqual<size_t>(2, MonitorRotation::GetRotatedMonitorIndex(0, 3, true));
|
||||
Assert::AreEqual<size_t>(1, MonitorRotation::GetRotatedMonitorIndex(2, 3, true));
|
||||
}
|
||||
|
||||
TEST_METHOD (MapsRectBetweenDifferentAndNegativeWorkAreas)
|
||||
{
|
||||
const RECT sourceWorkArea{ -1920, 0, 0, 1080 };
|
||||
const RECT targetWorkArea{ 0, -200, 2560, 1240 };
|
||||
const RECT sourceRect{ -1440, 270, -480, 810 };
|
||||
|
||||
const auto mapped = MonitorRotation::MapRectBetweenMonitorWorkAreas(sourceRect, sourceWorkArea, targetWorkArea);
|
||||
|
||||
Assert::AreEqual<LONG>(640, mapped.left);
|
||||
Assert::AreEqual<LONG>(160, mapped.top);
|
||||
Assert::AreEqual<LONG>(1920, mapped.right);
|
||||
Assert::AreEqual<LONG>(880, mapped.bottom);
|
||||
}
|
||||
|
||||
TEST_METHOD (ScaleCoordinateHandlesEmptySourceDimension)
|
||||
{
|
||||
Assert::AreEqual<LONG>(42, MonitorRotation::ScaleCoordinate(10, 0, 0, 42, 100));
|
||||
}
|
||||
|
||||
TEST_METHOD (ModifierReleaseDoesNotLeaveKeyPressed)
|
||||
{
|
||||
MonitorRotation::KeyState state;
|
||||
|
||||
for (const auto key : { VK_LCONTROL, VK_RCONTROL, VK_LSHIFT, VK_RSHIFT, VK_LWIN, VK_RWIN })
|
||||
{
|
||||
state.Update(key, true);
|
||||
Assert::IsTrue(state.IsDown(key));
|
||||
state.Update(key, false);
|
||||
Assert::IsFalse(state.IsDown(key));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_METHOD (ModifierStateKeepsOtherSidePressed)
|
||||
{
|
||||
MonitorRotation::KeyState state;
|
||||
state.Update(VK_LCONTROL, true);
|
||||
state.Update(VK_RCONTROL, true);
|
||||
state.Update(VK_LCONTROL, false);
|
||||
|
||||
Assert::IsTrue(state.IsAnyDown({ VK_LCONTROL, VK_RCONTROL }));
|
||||
|
||||
state.Update(VK_RCONTROL, false);
|
||||
Assert::IsFalse(state.IsAnyDown({ VK_LCONTROL, VK_RCONTROL }));
|
||||
}
|
||||
|
||||
TEST_METHOD (ConsumedKeyReleaseIsReportedOnce)
|
||||
{
|
||||
MonitorRotation::KeyState state;
|
||||
|
||||
Assert::IsTrue(state.Consume(VK_LEFT));
|
||||
Assert::IsFalse(state.Consume(VK_LEFT));
|
||||
Assert::IsTrue(state.ReleaseWasConsumed(VK_LEFT));
|
||||
Assert::IsFalse(state.ReleaseWasConsumed(VK_LEFT));
|
||||
}
|
||||
|
||||
TEST_METHOD (ResetClearsPressedAndConsumedKeys)
|
||||
{
|
||||
MonitorRotation::KeyState state;
|
||||
state.Update(VK_LCONTROL, true);
|
||||
state.Consume(VK_RIGHT);
|
||||
|
||||
state.Reset();
|
||||
|
||||
Assert::IsFalse(state.IsDown(VK_LCONTROL));
|
||||
Assert::IsFalse(state.ReleaseWasConsumed(VK_RIGHT));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
public const int VkOem3 = 0xc0;
|
||||
public const int VkNext = 0x22;
|
||||
public const int VkPrior = 0x21;
|
||||
public const int VkX = 0x58;
|
||||
|
||||
public static readonly HotkeySettings DefaultEditorHotkeyValue = new HotkeySettings(true, false, false, true, VkOem3);
|
||||
public static readonly HotkeySettings DefaultNextTabHotkeyValue = new HotkeySettings(true, false, false, false, VkNext);
|
||||
public static readonly HotkeySettings DefaultPrevTabHotkeyValue = new HotkeySettings(true, false, false, false, VkPrior);
|
||||
public static readonly HotkeySettings DefaultMonitorRotationHotkeyValue = new HotkeySettings(false, false, true, false, VkX);
|
||||
|
||||
public FZConfigProperties()
|
||||
{
|
||||
@@ -45,6 +47,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
FancyzonesWindowSwitching = new BoolProperty(true);
|
||||
FancyzonesNextTabHotkey = new KeyboardKeysProperty(DefaultNextTabHotkeyValue);
|
||||
FancyzonesPrevTabHotkey = new KeyboardKeysProperty(DefaultPrevTabHotkeyValue);
|
||||
FancyzonesMonitorRotation = new BoolProperty();
|
||||
FancyzonesMonitorRotationHotkey = new KeyboardKeysProperty(DefaultMonitorRotationHotkeyValue);
|
||||
FancyzonesMakeDraggedWindowTransparent = new BoolProperty();
|
||||
FancyzonesAllowPopupWindowSnap = new BoolProperty();
|
||||
FancyzonesAllowChildWindowSnap = new BoolProperty();
|
||||
@@ -139,6 +143,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("fancyzones_prevTab_hotkey")]
|
||||
public KeyboardKeysProperty FancyzonesPrevTabHotkey { get; set; }
|
||||
|
||||
[JsonPropertyName("fancyzones_monitorRotation")]
|
||||
public BoolProperty FancyzonesMonitorRotation { get; set; }
|
||||
|
||||
[JsonPropertyName("fancyzones_monitorRotation_hotkey")]
|
||||
public KeyboardKeysProperty FancyzonesMonitorRotationHotkey { get; set; }
|
||||
|
||||
[JsonPropertyName("fancyzones_excluded_apps")]
|
||||
public StringProperty FancyzonesExcludedApps { get; set; }
|
||||
|
||||
|
||||
@@ -215,6 +215,27 @@
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
|
||||
<tkcontrols:SettingsExpander
|
||||
Name="FancyZonesMonitorRotation"
|
||||
x:Uid="FancyZones_MonitorRotation"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<ToggleSwitch
|
||||
x:Uid="ToggleSwitch"
|
||||
AutomationProperties.Name="FancyZonesMonitorRotationToggle"
|
||||
IsOn="{x:Bind ViewModel.MonitorRotation, Mode=TwoWay}" />
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<!-- HACK: For some weird reason, a Shortcut Control is not working correctly if it's the first item in the expander, so we add an invisible card as the first one. -->
|
||||
<tkcontrols:SettingsCard Name="FancyZonesMonitorRotationPlaceholder" Visibility="Collapsed" />
|
||||
<tkcontrols:SettingsCard
|
||||
Name="FancyZonesMonitorRotationHotkeyControl"
|
||||
x:Uid="FancyZones_MonitorRotationHotkeyControl"
|
||||
IsEnabled="{x:Bind ViewModel.MonitorRotationCategoryEnabled, Mode=OneWay}">
|
||||
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.MonitorRotationHotkey, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
|
||||
<tkcontrols:SettingsExpander
|
||||
Name="FancyZonesOverrideSnapHotkeys"
|
||||
x:Uid="FancyZones_OverrideSnapHotkeys"
|
||||
@@ -310,4 +331,4 @@
|
||||
<controls:PageLink x:Uid="LearnMore_FancyZones" Link="https://aka.ms/PowerToysOverview_FancyZones" />
|
||||
</controls:SettingsPageControl.PrimaryLinks>
|
||||
</controls:SettingsPageControl>
|
||||
</local:NavigablePage>
|
||||
</local:NavigablePage>
|
||||
|
||||
@@ -912,6 +912,18 @@ opera.exe</value>
|
||||
<data name="FancyZones_HotkeyPrevTabControl.Header" xml:space="preserve">
|
||||
<value>Previous window</value>
|
||||
</data>
|
||||
<data name="FancyZones_MonitorRotation.Header" xml:space="preserve">
|
||||
<value>Rotate windows across monitors</value>
|
||||
</data>
|
||||
<data name="FancyZones_MonitorRotation.Description" xml:space="preserve">
|
||||
<value>Hold the shortcut and press Left or Right arrow to rotate open windows across all monitors.</value>
|
||||
</data>
|
||||
<data name="FancyZones_MonitorRotationHotkeyControl.Header" xml:space="preserve">
|
||||
<value>Rotation mode shortcut</value>
|
||||
</data>
|
||||
<data name="FancyZones_MonitorRotationHotkeyControl.Description" xml:space="preserve">
|
||||
<value>Hold this shortcut, then press Left or Right arrow to choose the rotation direction.</value>
|
||||
</data>
|
||||
<data name="FancyZones_LaunchEditorButtonControl.Header" xml:space="preserve">
|
||||
<value>Open layout editor</value>
|
||||
<comment>launches the FancyZones layout editor application</comment>
|
||||
@@ -6586,4 +6598,4 @@ Text uses the current drawing color.</value>
|
||||
<value>Example: outlook</value>
|
||||
<comment>{Locked="outlook"}</comment>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
||||
@@ -91,10 +91,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
_systemTheme = Settings.Properties.FancyzonesSystemTheme.Value;
|
||||
_showZoneNumber = Settings.Properties.FancyzonesShowZoneNumber.Value;
|
||||
_windowSwitching = Settings.Properties.FancyzonesWindowSwitching.Value;
|
||||
_monitorRotation = Settings.Properties.FancyzonesMonitorRotation.Value;
|
||||
|
||||
EditorHotkey = Settings.Properties.FancyzonesEditorHotkey.Value;
|
||||
NextTabHotkey = Settings.Properties.FancyzonesNextTabHotkey.Value;
|
||||
PrevTabHotkey = Settings.Properties.FancyzonesPrevTabHotkey.Value;
|
||||
MonitorRotationHotkey = Settings.Properties.FancyzonesMonitorRotationHotkey.Value;
|
||||
|
||||
// set the callback functions value to handle outgoing IPC message.
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
@@ -141,7 +143,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
var hotkeysDict = new Dictionary<string, HotkeySettings[]>
|
||||
{
|
||||
[ModuleName] = [EditorHotkey, NextTabHotkey, PrevTabHotkey],
|
||||
[ModuleName] = [EditorHotkey, NextTabHotkey, PrevTabHotkey, MonitorRotationHotkey],
|
||||
};
|
||||
|
||||
return hotkeysDict;
|
||||
@@ -180,6 +182,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
private bool _windowSwitching;
|
||||
private HotkeySettings _nextTabHotkey;
|
||||
private HotkeySettings _prevTabHotkey;
|
||||
private bool _monitorRotation;
|
||||
private HotkeySettings _monitorRotationHotkey;
|
||||
private string _zoneInActiveColor;
|
||||
private string _zoneBorderColor;
|
||||
private string _zoneHighlightColor;
|
||||
@@ -213,6 +217,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
OnPropertyChanged(nameof(SnapHotkeysCategoryEnabled));
|
||||
OnPropertyChanged(nameof(QuickSwitchEnabled));
|
||||
OnPropertyChanged(nameof(WindowSwitchingCategoryEnabled));
|
||||
OnPropertyChanged(nameof(MonitorRotationCategoryEnabled));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,6 +251,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool MonitorRotationCategoryEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
return _isEnabled && _monitorRotation;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShiftDrag
|
||||
{
|
||||
get
|
||||
@@ -863,6 +876,52 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool MonitorRotation
|
||||
{
|
||||
get
|
||||
{
|
||||
return _monitorRotation;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value != _monitorRotation)
|
||||
{
|
||||
_monitorRotation = value;
|
||||
|
||||
Settings.Properties.FancyzonesMonitorRotation.Value = _monitorRotation;
|
||||
NotifyPropertyChanged();
|
||||
OnPropertyChanged(nameof(MonitorRotationCategoryEnabled));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HotkeySettings MonitorRotationHotkey
|
||||
{
|
||||
get
|
||||
{
|
||||
return _monitorRotationHotkey;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value != _monitorRotationHotkey)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
_monitorRotationHotkey = FZConfigProperties.DefaultMonitorRotationHotkeyValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
_monitorRotationHotkey = value;
|
||||
}
|
||||
|
||||
Settings.Properties.FancyzonesMonitorRotationHotkey.Value = _monitorRotationHotkey;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ExcludedApps
|
||||
{
|
||||
get
|
||||
|
||||
Reference in New Issue
Block a user