diff --git a/src/modules/fancyzones/FancyZones/FancyZonesApp.cpp b/src/modules/fancyzones/FancyZones/FancyZonesApp.cpp index a20a6fc7f60e..44d0efcea26c 100644 --- a/src/modules/fancyzones/FancyZones/FancyZonesApp.cpp +++ b/src/modules/fancyzones/FancyZones/FancyZonesApp.cpp @@ -186,5 +186,11 @@ void FancyZonesApp::HandleWinHookEvent(WinHookEvent* data) noexcept intptr_t FancyZonesApp::HandleKeyboardHookEvent(LowlevelKeyboardEvent* data) noexcept { - return m_app.as()->OnKeyDown(data->lParam); + auto fzCallback = m_app.as(); + if (data->wParam == WM_KEYUP || data->wParam == WM_SYSKEYUP) + { + return fzCallback->OnKeyUp(data->lParam); + } + + return fzCallback->OnKeyDown(data->lParam); } diff --git a/src/modules/fancyzones/FancyZones/FancyZonesApp.h b/src/modules/fancyzones/FancyZones/FancyZonesApp.h index 44c6a306a9a2..a5f09801bc01 100644 --- a/src/modules/fancyzones/FancyZones/FancyZonesApp.h +++ b/src/modules/fancyzones/FancyZones/FancyZonesApp.h @@ -34,7 +34,7 @@ class FancyZonesApp 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(lParam); event.wParam = wParam; diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp b/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp index 4e83a450f5ab..4f94d1219a68 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp +++ b/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp @@ -66,11 +66,155 @@ 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{}; + }; + + 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), + }; + } + + std::optional FindMonitorIndex(HMONITOR monitor, const std::vector>& 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(std::distance(monitors.begin(), iter)); + } + + 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; + } + + std::vector CollectWindowRotationSnapshots() + { + std::vector 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*>(param); + snapshots.push_back({ .window = window, .sourceMonitor = sourceMonitor, .sourceRect = rect }); + return TRUE; + }, + reinterpret_cast(&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(std::stoul(deviceName.substr(digitStart + 1))); + } + catch (...) + { + return fallback; + } + } +} + struct FancyZones : public winrt::implements, public SettingsObserver { public: FancyZones(HINSTANCE hinstance, std::function 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); @@ -136,6 +280,8 @@ struct FancyZones : public winrt::implements& monitors, const GUID& virtualDesktop, const std::unordered_map>& workAreas) noexcept; void CycleWindows(bool reverse) noexcept; + void RotateWindowsAcrossMonitors(bool reverse) noexcept; + void ShowMonitorRotationPreview(std::optional reverse = std::nullopt, bool animateRotation = false) noexcept; + void HideMonitorRotationPreview() noexcept; + void EnsureMonitorRotationContentNumbers(const std::vector>& monitors) noexcept; + void RotateMonitorRotationContentNumbers(bool reverse) noexcept; + void UpdateMonitorRotationModifierState(DWORD vkCode, bool isDown) noexcept; + bool IsMonitorRotationActivatorKey(DWORD vkCode) const noexcept; + bool IsMonitorRotationChordDown() const noexcept; void SyncVirtualDesktops() noexcept; @@ -179,6 +333,14 @@ struct FancyZones : public winrt::implements m_pendingMonitorRotationReverse; + std::unordered_map m_monitorRotationContentNumbers; wil::unique_handle m_terminateEditorEvent; // Handle of FancyZonesEditor.exe we launch and wait on @@ -472,11 +634,41 @@ void FancyZones::WindowCreated(HWND window) noexcept IFACEMETHODIMP_(bool) FancyZones::OnKeyDown(PKBDLLHOOKSTRUCT info) noexcept { + UpdateMonitorRotationModifierState(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) + { + 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(reverse), 0); + } + return true; + } + + if (IsMonitorRotationActivatorKey(info->vkCode)) + { + return true; + } + + if (IsRotationPreviewReleaseKey(info->vkCode)) + { + return false; + } + } + if ((win && !shift && !ctrl) || (win && ctrl && alt)) { if ((info->vkCode == VK_RIGHT) || (info->vkCode == VK_LEFT) || (info->vkCode == VK_UP) || (info->vkCode == VK_DOWN)) @@ -526,6 +718,66 @@ 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); + UpdateMonitorRotationModifierState(info->vkCode, false); + + if (wasMonitorRotationPreviewActive && !IsMonitorRotationChordDown()) + { + PostMessageW(m_window, WM_PRIV_MONITOR_ROTATION_PREVIEW_HIDE, 0, 0); + return isActivatorKey; + } + + return false; +} + +void FancyZones::UpdateMonitorRotationModifierState(DWORD vkCode, bool isDown) noexcept +{ + if (IsWinKey(vkCode)) + { + m_monitorRotationWinDown = isDown; + } + else if (IsCtrlKey(vkCode)) + { + m_monitorRotationCtrlDown = isDown; + } + else if (IsAltKey(vkCode)) + { + m_monitorRotationAltDown = isDown; + } + else if (IsShiftKey(vkCode)) + { + m_monitorRotationShiftDown = isDown; + } + else if (IsMonitorRotationActivatorKey(vkCode)) + { + m_monitorRotationActivatorDown = isDown; + } +} + +bool FancyZones::IsMonitorRotationChordDown() const noexcept +{ + if (!FancyZonesSettings::settings().monitorRotation) + { + return false; + } + + const auto& hotkey = FancyZonesSettings::settings().monitorRotationHotkey; + return hotkey.win_pressed() == m_monitorRotationWinDown && + hotkey.ctrl_pressed() == m_monitorRotationCtrlDown && + hotkey.alt_pressed() == m_monitorRotationAltDown && + hotkey.shift_pressed() == m_monitorRotationShiftDown && + m_monitorRotationActivatorDown; +} + +bool FancyZones::IsMonitorRotationActivatorKey(DWORD vkCode) const noexcept +{ + return vkCode == FancyZonesSettings::settings().monitorRotationHotkey.get_code(); +} + void FancyZones::ToggleEditor() noexcept { _TRACER_; @@ -724,6 +976,37 @@ LRESULT FancyZones::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lpa { ApplyQuickLayout(static_cast(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(wparam); + m_pendingMonitorRotationReverse = reverse; + ShowMonitorRotationPreview(reverse, true); + SetTimer(m_window, MonitorRotationCommitTimerId, MonitorRotationCommitDelayMillis, nullptr); + } + 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(); @@ -993,6 +1276,145 @@ void FancyZones::CycleWindows(bool reverse) noexcept } } +void FancyZones::ShowMonitorRotationPreview(std::optional reverse, bool animateRotation) noexcept +{ + auto windows = CollectWindowRotationSnapshots(); + std::unordered_map> windowRectsByMonitor; + for (const auto& window : windows) + { + windowRectsByMonitor[window.sourceMonitor].push_back(window.sourceRect); + } + + auto monitors = FancyZonesUtils::GetAllMonitorRects<&MONITORINFOEX::rcWork>(); + FancyZonesUtils::OrderMonitors(monitors); + EnsureMonitorRotationContentNumbers(monitors); + + for (const auto& [monitor, workArea] : m_workAreaConfiguration.GetAllWorkAreas()) + { + if (!workArea) + { + continue; + } + + const auto iter = windowRectsByMonitor.find(monitor); + const std::vector 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>& 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 rotatedContentNumbers; + for (size_t sourceIndex = 0; sourceIndex < monitors.size(); sourceIndex++) + { + const size_t targetIndex = 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 = GetRotatedMonitorIndex(*sourceIndex, monitors.size(), reverse); + const RECT targetRect = MapRectBetweenMonitorWorkAreas( + window.sourceRect, + monitors[*sourceIndex].second, + monitors[targetIndex].second); + 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, @@ -1063,6 +1485,15 @@ void FancyZones::SettingsUpdate(SettingId id) UpdateHotkey(static_cast(HotkeyId::NextTab), FancyZonesSettings::settings().nextTabHotkey, FancyZonesSettings::settings().windowSwitching); } break; + case SettingId::MonitorRotation: + case SettingId::MonitorRotationHotkey: + { + if (m_monitorRotationPreviewActive && !IsMonitorRotationChordDown()) + { + HideMonitorRotationPreview(); + } + } + break; case SettingId::SpanZonesAcrossMonitors: { m_workAreaConfiguration.Clear(); diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZones.h b/src/modules/fancyzones/FancyZonesLib/FancyZones.h index 5cbcfd4df606..5f218011f025 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZones.h +++ b/src/modules/fancyzones/FancyZonesLib/FancyZones.h @@ -46,6 +46,14 @@ 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; }; winrt::com_ptr MakeFancyZones(HINSTANCE hinstance, std::function disableCallback) noexcept; diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesWinHookEventIDs.cpp b/src/modules/fancyzones/FancyZonesLib/FancyZonesWinHookEventIDs.cpp index 94c07ebd24e4..a7d13d0176ae 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesWinHookEventIDs.cpp +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesWinHookEventIDs.cpp @@ -19,6 +19,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; @@ -42,5 +45,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}"); }); } diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesWinHookEventIDs.h b/src/modules/fancyzones/FancyZonesLib/FancyZonesWinHookEventIDs.h index 214a5b1f75cc..ddb063274d26 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesWinHookEventIDs.h +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesWinHookEventIDs.h @@ -17,5 +17,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(); diff --git a/src/modules/fancyzones/FancyZonesLib/Settings.cpp b/src/modules/fancyzones/FancyZonesLib/Settings.cpp index 9cc886fe5cd4..275d06178b55 100644 --- a/src/modules/fancyzones/FancyZonesLib/Settings.cpp +++ b/src/modules/fancyzones/FancyZonesLib/Settings.cpp @@ -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)) { diff --git a/src/modules/fancyzones/FancyZonesLib/Settings.h b/src/modules/fancyzones/FancyZonesLib/Settings.h index a9bcbeaa172f..871b0bb9cf30 100644 --- a/src/modules/fancyzones/FancyZonesLib/Settings.h +++ b/src/modules/fancyzones/FancyZonesLib/Settings.h @@ -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 excludedAppsArray; }; diff --git a/src/modules/fancyzones/FancyZonesLib/SettingsConstants.h b/src/modules/fancyzones/FancyZonesLib/SettingsConstants.h index a789d864848d..e4267c881eac 100644 --- a/src/modules/fancyzones/FancyZonesLib/SettingsConstants.h +++ b/src/modules/fancyzones/FancyZonesLib/SettingsConstants.h @@ -31,7 +31,9 @@ enum class SettingId WindowSwitching, NextTabHotkey, PrevTabHotkey, + MonitorRotation, + MonitorRotationHotkey, ExcludedApps, AllowSnapChildWindows, DisableRoundCornersOnSnapping, -}; \ No newline at end of file +}; diff --git a/src/modules/fancyzones/FancyZonesLib/WorkArea.cpp b/src/modules/fancyzones/FancyZonesLib/WorkArea.cpp index a2cae59ca8ab..a733d479d3d6 100644 --- a/src/modules/fancyzones/FancyZonesLib/WorkArea.cpp +++ b/src/modules/fancyzones/FancyZonesLib/WorkArea.cpp @@ -199,6 +199,30 @@ void WorkArea::FlashZones() } } +void WorkArea::ShowMonitorRotationPreview(const std::vector& windowRects, size_t monitorNumber, std::optional reverse, bool animateRotation) +{ + if (!m_zonesOverlay) + { + return; + } + + std::vector 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({}); @@ -358,4 +382,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); -} \ No newline at end of file +} diff --git a/src/modules/fancyzones/FancyZonesLib/WorkArea.h b/src/modules/fancyzones/FancyZonesLib/WorkArea.h index ecd97fd8490c..092a6ea5c22d 100644 --- a/src/modules/fancyzones/FancyZonesLib/WorkArea.h +++ b/src/modules/fancyzones/FancyZonesLib/WorkArea.h @@ -1,5 +1,8 @@ #pragma once +#include +#include + #include #include @@ -53,6 +56,7 @@ class WorkArea void ShowZones(const ZoneIndexSet& highlight, HWND draggedWindow = nullptr); void HideZones(); void FlashZones(); + void ShowMonitorRotationPreview(const std::vector& windowRects, size_t monitorNumber, std::optional reverse, bool animateRotation); void CycleWindows(HWND window, bool reverse); diff --git a/src/modules/fancyzones/FancyZonesLib/ZonesOverlay.cpp b/src/modules/fancyzones/FancyZonesLib/ZonesOverlay.cpp index bb6896c3bbf6..864f809465bf 100644 --- a/src/modules/fancyzones/FancyZonesLib/ZonesOverlay.cpp +++ b/src/modules/fancyzones/FancyZonesLib/ZonesOverlay.cpp @@ -2,6 +2,7 @@ #include "ZonesOverlay.h" #include +#include #include #include #include @@ -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; @@ -128,8 +211,43 @@ ZonesOverlay::RenderResult ZonesOverlay::Render() m_renderTarget->BeginDraw(); + const auto renderNow = std::chrono::steady_clock().now(); + float rotationPulse = 0.f; + float rotationProgress = 0.f; + if (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(m_clientRect.right - m_clientRect.left); + const auto height = static_cast(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 = m_animateRotation ? directionSign * easedRotationProgress * ClampDimension(width * 0.075f, 44.f, 132.f) : 0.f; + const float pulseScale = 1.f + ((m_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 +260,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 +287,7 @@ ZonesOverlay::RenderResult ZonesOverlay::Render() if (drawableRect.showText) { + ID2D1SolidColorBrush* textBrush = nullptr; m_renderTarget->CreateSolidColorBrush(drawableRect.textColor, &textBrush); if (textFormat && textBrush) @@ -190,6 +309,153 @@ ZonesOverlay::RenderResult ZonesOverlay::Render() textFormat->Release(); } + const auto accentColor = MonitorAccentColor(m_monitorNumber.value_or(1)); + + if (m_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(MarkerCount - 1 - markerIndex) : static_cast(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 = m_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(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 +552,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 +609,47 @@ void ZonesOverlay::DrawActiveZoneSet(const ZonesMap& zones, } } +void ZonesOverlay::DrawMonitorRotationPreview(const std::vector& windowRects, size_t monitorNumber, std::optional 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() { { diff --git a/src/modules/fancyzones/FancyZonesLib/ZonesOverlay.h b/src/modules/fancyzones/FancyZonesLib/ZonesOverlay.h index fa20f5edc5a7..7830b1a38d2e 100644 --- a/src/modules/fancyzones/FancyZonesLib/ZonesOverlay.h +++ b/src/modules/fancyzones/FancyZonesLib/ZonesOverlay.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -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 m_sceneRects; + bool m_drawBackdrop = false; + RotationDirection m_rotationDirection = RotationDirection::None; + bool m_animateRotation = false; + std::optional m_monitorNumber; + std::optional 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 @@ class ZonesOverlay const ZoneIndexSet& highlightZones, const Colors::ZoneColors& colors, const bool showZoneText); + void DrawMonitorRotationPreview(const std::vector& windowRects, size_t monitorNumber, std::optional reverse, bool animateRotation); }; diff --git a/src/modules/fancyzones/FancyZonesTests/UnitTests/FancyZonesSettings.Spec.cpp b/src/modules/fancyzones/FancyZonesTests/UnitTests/FancyZonesSettings.Spec.cpp index 8c93e5a05cee..3d8beaf7d535 100644 --- a/src/modules/fancyzones/FancyZonesTests/UnitTests/FancyZonesSettings.Spec.cpp +++ b/src/modules/fancyzones/FancyZonesTests/UnitTests/FancyZonesSettings.Spec.cpp @@ -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()); diff --git a/src/settings-ui/Settings.UI.Library/FZConfigProperties.cs b/src/settings-ui/Settings.UI.Library/FZConfigProperties.cs index 9150cdbe90b5..a343b0dbacc6 100644 --- a/src/settings-ui/Settings.UI.Library/FZConfigProperties.cs +++ b/src/settings-ui/Settings.UI.Library/FZConfigProperties.cs @@ -15,10 +15,12 @@ public class FZConfigProperties 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 @@ public FZConfigProperties() 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 @@ public FZConfigProperties() [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; } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml index 1d6e95f9745e..effdece3472e 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml @@ -215,6 +215,27 @@ + + + + + + + + + + + - \ No newline at end of file + diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index 76042ede6d7a..0a72db583234 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -852,6 +852,18 @@ opera.exe Previous window + + Rotate windows across monitors + + + Hold the shortcut and press Left or Right arrow to rotate open windows across all monitors. + + + Rotation mode shortcut + + + Hold this shortcut, then press Left or Right arrow to choose the rotation direction. + Open layout editor launches the FancyZones layout editor application @@ -6199,4 +6211,4 @@ Text uses the current drawing color. Example: outlook {Locked="outlook"} - \ No newline at end of file + diff --git a/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs index 3fa21824e6ff..0dfc67bb5bb1 100644 --- a/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs @@ -91,10 +91,12 @@ public FancyZonesViewModel(SettingsUtils settingsUtils, ISettingsRepository GetAllHotkeySettings() { var hotkeysDict = new Dictionary { - [ModuleName] = [EditorHotkey, NextTabHotkey, PrevTabHotkey], + [ModuleName] = [EditorHotkey, NextTabHotkey, PrevTabHotkey, MonitorRotationHotkey], }; return hotkeysDict; @@ -180,6 +182,8 @@ public override Dictionary GetAllHotkeySettings() 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 @@ public bool IsEnabled OnPropertyChanged(nameof(SnapHotkeysCategoryEnabled)); OnPropertyChanged(nameof(QuickSwitchEnabled)); OnPropertyChanged(nameof(WindowSwitchingCategoryEnabled)); + OnPropertyChanged(nameof(MonitorRotationCategoryEnabled)); } } } @@ -246,6 +251,14 @@ public bool WindowSwitchingCategoryEnabled } } + public bool MonitorRotationCategoryEnabled + { + get + { + return _isEnabled && _monitorRotation; + } + } + public bool ShiftDrag { get @@ -863,6 +876,52 @@ public HotkeySettings PrevTabHotkey } } + 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